├── .gitignore ├── LICENSE ├── Linkage.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Linkage ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── CollectionView │ ├── CollectionCategoryModel.swift │ ├── CollectionViewCell.swift │ ├── CollectionViewController.swift │ └── CollectionViewHeaderView.swift ├── Global.swift ├── Info.plist ├── Kingfisher │ ├── AnimatedImageView.swift │ ├── Box.swift │ ├── CacheSerializer.swift │ ├── Filter.swift │ ├── Image.swift │ ├── ImageCache.swift │ ├── ImageDownloader.swift │ ├── ImagePrefetcher.swift │ ├── ImageProcessor.swift │ ├── ImageTransition.swift │ ├── ImageView+Kingfisher.swift │ ├── Indicator.swift │ ├── Kingfisher.h │ ├── Kingfisher.swift │ ├── KingfisherManager.swift │ ├── KingfisherOptionsInfo.swift │ ├── RequestModifier.swift │ ├── Resource.swift │ ├── String+MD5.swift │ ├── ThreadHelper.swift │ └── UIButton+Kingfisher.swift ├── LaunchScreen.storyboard ├── TableView │ ├── CategoryModel.swift │ ├── LeftTableViewCell.swift │ ├── RightTableViewCell.swift │ ├── TableViewController.swift │ └── TableViewHeaderView.swift ├── Tools │ ├── LJCollectionViewFlowLayout.swift │ └── UIColor-Extension.swift ├── ViewController.swift ├── liwushuo.json └── meituan.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Linkage.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Linkage/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/2/28. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Linkage/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Linkage/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Linkage/CollectionView/CollectionCategoryModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionCategoryModel.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/10. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionCategoryModel: NSObject { 12 | 13 | var name : String? 14 | var subcategories : [SubCategoryModel]? 15 | 16 | init(dict : [String : Any]) { 17 | super.init() 18 | setValuesForKeys(dict) 19 | } 20 | 21 | override func setValue(_ value: Any?, forKey key: String) { 22 | 23 | if key == "subcategories" { 24 | subcategories = Array() 25 | guard let datas = value as? [[String : Any]] else { return } 26 | for dict in datas { 27 | let subModel = SubCategoryModel(dict: dict) 28 | subcategories?.append(subModel) 29 | } 30 | } else { 31 | super.setValue(value, forKey: key) 32 | } 33 | 34 | } 35 | 36 | override func setValue(_ value: Any?, forUndefinedKey key: String) { 37 | 38 | } 39 | 40 | } 41 | 42 | class SubCategoryModel: NSObject { 43 | 44 | var iconUrl : String? 45 | var name : String? 46 | 47 | init(dict : [String : Any]) { 48 | super.init() 49 | setValuesForKeys(dict) 50 | } 51 | 52 | override func setValue(_ value: Any?, forKey key: String) { 53 | if key == "icon_url" { 54 | guard let icon = value as? String else { return } 55 | iconUrl = icon 56 | } else { 57 | super.setValue(value, forKey: key) 58 | } 59 | } 60 | 61 | override func setValue(_ value: Any?, forUndefinedKey key: String) { 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Linkage/CollectionView/CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/13. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionViewCell: UICollectionViewCell { 12 | 13 | private lazy var imageV = UIImageView() 14 | private lazy var nameLabel = UILabel() 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | 19 | configureUI() 20 | } 21 | 22 | func configureUI() { 23 | imageV.frame = CGRect(x: 2, y: 2, width: frame.size.width - 4, height: frame.size.width - 4) 24 | imageV.contentMode = .scaleAspectFit 25 | contentView.addSubview(imageV) 26 | 27 | nameLabel.frame = CGRect.init(x: 2, y: frame.size.width + 2, width: frame.size.width - 4, height: 20) 28 | nameLabel.font = UIFont.systemFont(ofSize: 13) 29 | nameLabel.textAlignment = .center 30 | contentView.addSubview(nameLabel) 31 | } 32 | 33 | func setDatas(_ model : SubCategoryModel) { 34 | 35 | guard 36 | let iconUrl = model.iconUrl, 37 | let name = model.name else { return } 38 | 39 | guard let url = URL(string: iconUrl) else { return } 40 | 41 | nameLabel.text = name 42 | imageV.kf.setImage(with: url) 43 | } 44 | 45 | required init?(coder aDecoder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Linkage/CollectionView/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewController.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/10. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { 12 | 13 | fileprivate lazy var tableView : UITableView = { 14 | let tableView = UITableView() 15 | tableView.delegate = self 16 | tableView.dataSource = self 17 | tableView.frame = CGRect(x: 0, y: 0, width: 80, height: ScreenHeight) 18 | tableView.rowHeight = 55 19 | tableView.showsVerticalScrollIndicator = false 20 | tableView.separatorColor = UIColor.clear 21 | tableView.register(LeftTableViewCell.self, forCellReuseIdentifier: kLeftTableViewCell) 22 | return tableView 23 | }() 24 | 25 | fileprivate lazy var flowlayout : LJCollectionViewFlowLayout = { 26 | let flowlayout = LJCollectionViewFlowLayout() 27 | flowlayout.scrollDirection = .vertical 28 | flowlayout.minimumLineSpacing = 2 29 | flowlayout.minimumInteritemSpacing = 2 30 | flowlayout.itemSize = CGSize(width: (ScreenWidth - 80 - 4 - 4) / 3, height: (ScreenWidth - 80 - 4 - 4) / 3 + 30) 31 | return flowlayout 32 | }() 33 | 34 | fileprivate lazy var collectionView : UICollectionView = { 35 | let collectionView = UICollectionView(frame: CGRect.init(x: 2 + 80, y: 2 + 64, width: ScreenWidth - 80 - 4, height: ScreenHeight - 64 - 4), collectionViewLayout: self.flowlayout) 36 | collectionView.delegate = self 37 | collectionView.dataSource = self 38 | collectionView.showsVerticalScrollIndicator = false 39 | collectionView.showsHorizontalScrollIndicator = false 40 | collectionView.backgroundColor = UIColor.clear 41 | collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: kCollectionViewCell) 42 | collectionView.register(CollectionViewHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: kCollectionViewHeaderView) 43 | return collectionView 44 | }() 45 | 46 | fileprivate lazy var dataSource = [CollectionCategoryModel]() 47 | fileprivate lazy var collectionDatas = [[SubCategoryModel]]() 48 | 49 | fileprivate var selectIndex = 0 50 | fileprivate var isScrollDown = true 51 | fileprivate var lastOffsetY : CGFloat = 0.0 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | view.backgroundColor = UIColor.white 57 | 58 | configureData() 59 | 60 | view.addSubview(tableView) 61 | view.addSubview(collectionView) 62 | 63 | tableView.selectRow(at: IndexPath(row: 0, section: 0), animated: true, scrollPosition: .none) 64 | } 65 | } 66 | 67 | //MARK: - 获取数据 68 | extension CollectionViewController { 69 | 70 | func configureData() { 71 | 72 | guard let path = Bundle.main.path(forResource: "liwushuo", ofType: "json") else { return } 73 | 74 | guard let data = NSData(contentsOfFile: path) as Data? else { return } 75 | 76 | guard let anyObject = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else { return } 77 | 78 | guard let dict = anyObject as? [String : Any] else { return } 79 | 80 | guard let datas = dict["data"] as? [String : Any] else { return } 81 | 82 | guard let categories = datas["categories"] as? [[String : Any]] else { return } 83 | 84 | for category in categories { 85 | let model = CollectionCategoryModel(dict: category) 86 | dataSource.append(model) 87 | 88 | guard let subcategories = model.subcategories else { continue } 89 | 90 | var datas = [SubCategoryModel]() 91 | for subcategory in subcategories { 92 | datas.append(subcategory) 93 | } 94 | collectionDatas.append(datas) 95 | } 96 | 97 | } 98 | } 99 | 100 | //MARK: - TableView DataSource Delegate 101 | extension CollectionViewController { 102 | 103 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 104 | return dataSource.count 105 | } 106 | 107 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 108 | let cell = tableView.dequeueReusableCell(withIdentifier: kLeftTableViewCell, for: indexPath) as! LeftTableViewCell 109 | let model = dataSource[indexPath.row] 110 | cell.nameLabel.text = model.name 111 | return cell 112 | } 113 | 114 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 115 | selectIndex = indexPath.row 116 | 117 | // http://stackoverflow.com/questions/22100227/scroll-uicollectionview-to-section-header-view 118 | // 解决点击 TableView 后 CollectionView 的 Header 遮挡问题。 119 | scrollToTop(section: selectIndex, animated: true) 120 | 121 | // collectionView.scrollToItem(at: IndexPath(row: 0, section: selectIndex), at: .top, animated: true) 122 | tableView.scrollToRow(at: IndexPath(row: selectIndex, section: 0), at: .top, animated: true) 123 | } 124 | 125 | //MARK: - 解决点击 TableView 后 CollectionView 的 Header 遮挡问题。 126 | fileprivate func scrollToTop(section: Int, animated: Bool) { 127 | let headerRect = frameForHeader(section: section) 128 | let topOfHeader = CGPoint(x: 0, y: headerRect.origin.y - collectionView.contentInset.top) 129 | collectionView.setContentOffset(topOfHeader, animated: animated) 130 | } 131 | 132 | fileprivate func frameForHeader(section: Int) -> CGRect { 133 | let indexPath = IndexPath(item: 0, section: section) 134 | let attributes = collectionView.collectionViewLayout.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: indexPath) 135 | guard let frameForFirstCell = attributes?.frame else { 136 | return .zero 137 | } 138 | return frameForFirstCell; 139 | } 140 | } 141 | 142 | //MARK: - CollectionView DataSource Delegate 143 | extension CollectionViewController { 144 | 145 | func numberOfSections(in collectionView: UICollectionView) -> Int { 146 | return dataSource.count 147 | } 148 | 149 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 150 | return collectionDatas[section].count 151 | } 152 | 153 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 154 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCollectionViewCell, for: indexPath) as! CollectionViewCell 155 | let model = collectionDatas[indexPath.section][indexPath.row] 156 | cell.setDatas(model) 157 | return cell 158 | } 159 | 160 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 161 | var reuseIdentifier : String? 162 | if kind == UICollectionElementKindSectionHeader { 163 | reuseIdentifier = kCollectionViewHeaderView 164 | } 165 | 166 | let view = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: reuseIdentifier!, for: indexPath) as! CollectionViewHeaderView 167 | 168 | if kind == UICollectionElementKindSectionHeader { 169 | let model = dataSource[indexPath.section] 170 | view.setDatas(model) 171 | } 172 | return view 173 | } 174 | 175 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 176 | return CGSize(width: ScreenWidth, height: 30) 177 | } 178 | 179 | // CollectionView 分区标题即将展示 180 | func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) { 181 | // 当前 CollectionView 滚动的方向向上,CollectionView 是用户拖拽而产生滚动的(主要是判断 CollectionView 是用户拖拽而滚动的,还是点击 TableView 而滚动的) 182 | if !isScrollDown && (collectionView.isDragging || collectionView.isDecelerating) { 183 | selectRow(index: indexPath.section) 184 | } 185 | } 186 | 187 | // CollectionView 分区标题展示结束 188 | func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) { 189 | // 当前 CollectionView 滚动的方向向下,CollectionView 是用户拖拽而产生滚动的(主要是判断 CollectionView 是用户拖拽而滚动的,还是点击 TableView 而滚动的) 190 | if isScrollDown && (collectionView.isDragging || collectionView.isDecelerating) { 191 | selectRow(index: indexPath.section + 1) 192 | } 193 | } 194 | 195 | // 当拖动 CollectionView 的时候,处理 TableView 196 | private func selectRow(index : Int) { 197 | tableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .middle) 198 | } 199 | 200 | // 标记一下 CollectionView 的滚动方向,是向上还是向下 201 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 202 | if collectionView == scrollView { 203 | isScrollDown = lastOffsetY < scrollView.contentOffset.y 204 | lastOffsetY = scrollView.contentOffset.y 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /Linkage/CollectionView/CollectionViewHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewHeaderView.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/13. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionViewHeaderView: UICollectionReusableView { 12 | 13 | private lazy var titleLabel = UILabel() 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | 18 | backgroundColor = UIColor(240, 240, 240, 0.8) 19 | 20 | titleLabel.frame = CGRect(x: 0, y: 5, width: ScreenWidth - 80, height: 20) 21 | titleLabel.font = UIFont.systemFont(ofSize: 14) 22 | titleLabel.textAlignment = .center 23 | addSubview(titleLabel) 24 | } 25 | 26 | func setDatas(_ model : CollectionCategoryModel) { 27 | 28 | guard let name = model.name else { return } 29 | 30 | titleLabel.text = name 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Linkage/Global.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Global.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/10. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let ScreenWidth = UIScreen.main.bounds.width 12 | let ScreenHeight = UIScreen.main.bounds.height 13 | 14 | let kLeftTableViewCell = "LeftTableViewCell" 15 | let kRightTableViewCell = "RightTableViewCell" 16 | let kCollectionViewCell = "CollectionViewCell" 17 | let kCollectionViewHeaderView = "CollectionViewHeaderView" 18 | -------------------------------------------------------------------------------- /Linkage/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | NSAppTransportSecurity 45 | 46 | NSAllowsArbitraryLoads 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/AnimatedImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableImageView.swift 3 | // Kingfisher 4 | // 5 | // Created by bl4ckra1sond3tre on 4/22/16. 6 | // 7 | // The AnimatableImageView, AnimatedFrame and Animator is a modified version of 8 | // some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu) 9 | // 10 | // The MIT License (MIT) 11 | // 12 | // Copyright (c) 2017 Reda Lemeden. 13 | // 14 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 15 | // this software and associated documentation files (the "Software"), to deal in 16 | // the Software without restriction, including without limitation the rights to 17 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 18 | // the Software, and to permit persons to whom the Software is furnished to do so, 19 | // subject to the following conditions: 20 | // 21 | // The above copyright notice and this permission notice shall be included in all 22 | // copies or substantial portions of the Software. 23 | // 24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 26 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 27 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 28 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 29 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | // 31 | // The name and characters used in the demo of this software are property of their 32 | // respective owners. 33 | 34 | import UIKit 35 | import ImageIO 36 | 37 | /// `AnimatedImageView` is a subclass of `UIImageView` for displaying animated image. 38 | open class AnimatedImageView: UIImageView { 39 | 40 | /// Proxy object for prevending a reference cycle between the CADDisplayLink and AnimatedImageView. 41 | class TargetProxy { 42 | private weak var target: AnimatedImageView? 43 | 44 | init(target: AnimatedImageView) { 45 | self.target = target 46 | } 47 | 48 | @objc func onScreenUpdate() { 49 | target?.updateFrame() 50 | } 51 | } 52 | 53 | // MARK: - Public property 54 | /// Whether automatically play the animation when the view become visible. Default is true. 55 | public var autoPlayAnimatedImage = true 56 | 57 | /// The size of the frame cache. 58 | public var framePreloadCount = 10 59 | 60 | /// Specifies whether the GIF frames should be pre-scaled to save memory. Default is true. 61 | public var needsPrescaling = true 62 | 63 | /// The animation timer's run loop mode. Default is `NSRunLoopCommonModes`. Set this property to `NSDefaultRunLoopMode` will make the animation pause during UIScrollView scrolling. 64 | public var runLoopMode = RunLoopMode.commonModes { 65 | willSet { 66 | if runLoopMode == newValue { 67 | return 68 | } else { 69 | stopAnimating() 70 | displayLink.remove(from: .main, forMode: runLoopMode) 71 | displayLink.add(to: .main, forMode: newValue) 72 | startAnimating() 73 | } 74 | } 75 | } 76 | 77 | // MARK: - Private property 78 | /// `Animator` instance that holds the frames of a specific image in memory. 79 | private var animator: Animator? 80 | 81 | /// A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. :D 82 | private var isDisplayLinkInitialized: Bool = false 83 | 84 | /// A display link that keeps calling the `updateFrame` method on every screen refresh. 85 | private lazy var displayLink: CADisplayLink = { 86 | self.isDisplayLinkInitialized = true 87 | let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate)) 88 | displayLink.add(to: .main, forMode: self.runLoopMode) 89 | displayLink.isPaused = true 90 | return displayLink 91 | }() 92 | 93 | // MARK: - Override 94 | override open var image: Image? { 95 | didSet { 96 | if image != oldValue { 97 | reset() 98 | } 99 | setNeedsDisplay() 100 | layer.setNeedsDisplay() 101 | } 102 | } 103 | 104 | deinit { 105 | if isDisplayLinkInitialized { 106 | displayLink.invalidate() 107 | } 108 | } 109 | 110 | override open var isAnimating: Bool { 111 | if isDisplayLinkInitialized { 112 | return !displayLink.isPaused 113 | } else { 114 | return super.isAnimating 115 | } 116 | } 117 | 118 | /// Starts the animation. 119 | override open func startAnimating() { 120 | if self.isAnimating { 121 | return 122 | } else { 123 | displayLink.isPaused = false 124 | } 125 | } 126 | 127 | /// Stops the animation. 128 | override open func stopAnimating() { 129 | super.stopAnimating() 130 | if isDisplayLinkInitialized { 131 | displayLink.isPaused = true 132 | } 133 | } 134 | 135 | override open func display(_ layer: CALayer) { 136 | if let currentFrame = animator?.currentFrame { 137 | layer.contents = currentFrame.cgImage 138 | } else { 139 | layer.contents = image?.cgImage 140 | } 141 | } 142 | 143 | override open func didMoveToWindow() { 144 | super.didMoveToWindow() 145 | didMove() 146 | } 147 | 148 | override open func didMoveToSuperview() { 149 | super.didMoveToSuperview() 150 | didMove() 151 | } 152 | 153 | // This is for back compatibility that using regular UIImageView to show GIF. 154 | override func shouldPreloadAllGIF() -> Bool { 155 | return false 156 | } 157 | 158 | // MARK: - Private method 159 | /// Reset the animator. 160 | private func reset() { 161 | animator = nil 162 | if let imageSource = image?.kf.imageSource?.imageRef { 163 | animator = Animator(imageSource: imageSource, contentMode: contentMode, size: bounds.size, framePreloadCount: framePreloadCount) 164 | animator?.needsPrescaling = needsPrescaling 165 | animator?.prepareFramesAsynchronously() 166 | } 167 | didMove() 168 | } 169 | 170 | private func didMove() { 171 | if autoPlayAnimatedImage && animator != nil { 172 | if let _ = superview, let _ = window { 173 | startAnimating() 174 | } else { 175 | stopAnimating() 176 | } 177 | } 178 | } 179 | 180 | /// Update the current frame with the displayLink duration. 181 | private func updateFrame() { 182 | if animator?.updateCurrentFrame(duration: displayLink.duration) ?? false { 183 | layer.setNeedsDisplay() 184 | } 185 | } 186 | } 187 | 188 | /// Keeps a reference to an `Image` instance and its duration as a GIF frame. 189 | struct AnimatedFrame { 190 | var image: Image? 191 | let duration: TimeInterval 192 | 193 | static let null: AnimatedFrame = AnimatedFrame(image: .none, duration: 0.0) 194 | } 195 | 196 | // MARK: - Animator 197 | class Animator { 198 | // MARK: Private property 199 | fileprivate let size: CGSize 200 | fileprivate let maxFrameCount: Int 201 | fileprivate let imageSource: CGImageSource 202 | 203 | fileprivate var animatedFrames = [AnimatedFrame]() 204 | fileprivate let maxTimeStep: TimeInterval = 1.0 205 | fileprivate var frameCount = 0 206 | fileprivate var currentFrameIndex = 0 207 | fileprivate var currentPreloadIndex = 0 208 | fileprivate var timeSinceLastFrameChange: TimeInterval = 0.0 209 | fileprivate var needsPrescaling = true 210 | 211 | /// Loop count of animatd image. 212 | private var loopCount = 0 213 | 214 | var currentFrame: UIImage? { 215 | return frame(at: currentFrameIndex) 216 | } 217 | 218 | var contentMode = UIViewContentMode.scaleToFill 219 | 220 | private lazy var preloadQueue: DispatchQueue = { 221 | return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") 222 | }() 223 | 224 | /** 225 | Init an animator with image source reference. 226 | 227 | - parameter imageSource: The reference of animated image. 228 | - parameter contentMode: Content mode of AnimatedImageView. 229 | - parameter size: Size of AnimatedImageView. 230 | - parameter framePreloadCount: Frame cache size. 231 | 232 | - returns: The animator object. 233 | */ 234 | init(imageSource source: CGImageSource, contentMode mode: UIViewContentMode, size: CGSize, framePreloadCount count: Int) { 235 | self.imageSource = source 236 | self.contentMode = mode 237 | self.size = size 238 | self.maxFrameCount = count 239 | } 240 | 241 | func frame(at index: Int) -> Image? { 242 | return animatedFrames[safe: index]?.image 243 | } 244 | 245 | func prepareFramesAsynchronously() { 246 | preloadQueue.async { [weak self] in 247 | self?.prepareFrames() 248 | } 249 | } 250 | 251 | private func prepareFrames() { 252 | frameCount = CGImageSourceGetCount(imageSource) 253 | 254 | if let properties = CGImageSourceCopyProperties(imageSource, nil), 255 | let gifInfo = (properties as NSDictionary)[kCGImagePropertyGIFDictionary as String] as? NSDictionary, 256 | let loopCount = gifInfo[kCGImagePropertyGIFLoopCount as String] as? Int 257 | { 258 | self.loopCount = loopCount 259 | } 260 | 261 | let frameToProcess = min(frameCount, maxFrameCount) 262 | animatedFrames.reserveCapacity(frameToProcess) 263 | animatedFrames = (0.. AnimatedFrame { 268 | 269 | guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else { 270 | return AnimatedFrame.null 271 | } 272 | 273 | let defaultGIFFrameDuration = 0.100 274 | let frameDuration = imageSource.kf.gifProperties(at: index).map { 275 | gifInfo -> Double in 276 | 277 | let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as Double? 278 | let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as Double? 279 | let duration = unclampedDelayTime ?? delayTime ?? 0.0 280 | 281 | /** 282 | http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp 283 | Many annoying ads specify a 0 duration to make an image flash as quickly as 284 | possible. We follow Safari and Firefox's behavior and use a duration of 100 ms 285 | for any frames that specify a duration of <= 10 ms. 286 | See and for more information. 287 | 288 | See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser. 289 | */ 290 | return duration > 0.011 ? duration : defaultGIFFrameDuration 291 | } ?? defaultGIFFrameDuration 292 | 293 | let image = Image(cgImage: imageRef) 294 | let scaledImage: Image? 295 | 296 | if needsPrescaling { 297 | scaledImage = image.kf.resize(to: size, for: contentMode) 298 | } else { 299 | scaledImage = image 300 | } 301 | 302 | return AnimatedFrame(image: scaledImage, duration: frameDuration) 303 | } 304 | 305 | /** 306 | Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`. 307 | */ 308 | func updateCurrentFrame(duration: CFTimeInterval) -> Bool { 309 | timeSinceLastFrameChange += min(maxTimeStep, duration) 310 | guard let frameDuration = animatedFrames[safe: currentFrameIndex]?.duration, frameDuration <= timeSinceLastFrameChange else { 311 | return false 312 | } 313 | 314 | timeSinceLastFrameChange -= frameDuration 315 | 316 | let lastFrameIndex = currentFrameIndex 317 | currentFrameIndex += 1 318 | currentFrameIndex = currentFrameIndex % animatedFrames.count 319 | 320 | if animatedFrames.count < frameCount { 321 | preloadFrameAsynchronously(at: lastFrameIndex) 322 | } 323 | return true 324 | } 325 | 326 | private func preloadFrameAsynchronously(at index: Int) { 327 | preloadQueue.async { [weak self] in 328 | self?.preloadFrame(at: index) 329 | } 330 | } 331 | 332 | private func preloadFrame(at index: Int) { 333 | animatedFrames[index] = prepareFrame(at: currentPreloadIndex) 334 | currentPreloadIndex += 1 335 | currentPreloadIndex = currentPreloadIndex % frameCount 336 | } 337 | } 338 | 339 | extension CGImageSource: KingfisherCompatible { } 340 | extension Kingfisher where Base: CGImageSource { 341 | func gifProperties(at index: Int) -> [String: Double]? { 342 | let properties = CGImageSourceCopyPropertiesAtIndex(base, index, nil) as Dictionary? 343 | return properties?[kCGImagePropertyGIFDictionary] as? [String: Double] 344 | } 345 | } 346 | 347 | extension Array { 348 | subscript(safe index: Int) -> Element? { 349 | return indices ~= index ? self[index] : nil 350 | } 351 | } 352 | 353 | private func pure(_ value: T) -> [T] { 354 | return [value] 355 | } 356 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/Box.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Box.swift 3 | // Kingfisher 4 | // 5 | // Created by WANG WEI on 2016/09/12. 6 | // Copyright © 2016年 Wei Wang. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Box { 12 | let value: T 13 | init(value: T) { 14 | self.value = value 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/CacheSerializer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheSerializer.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 2016/09/02. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | /// An `CacheSerializer` would be used to convert some data to an image object for 30 | /// retrieving from disk cache and vice versa for storing to disk cache. 31 | public protocol CacheSerializer { 32 | 33 | /// Get the serialized data from a provided image 34 | /// and optional original data for caching to disk. 35 | /// 36 | /// 37 | /// - parameter image: The image needed to be serialized. 38 | /// - parameter original: The original data which is just downloaded. 39 | /// If the image is retrieved from cache instead of 40 | /// downloaded, it will be `nil`. 41 | /// 42 | /// - returns: A data which will be stored to cache, or `nil` when no valid 43 | /// data could be serialized. 44 | func data(with image: Image, original: Data?) -> Data? 45 | 46 | /// Get an image deserialized from provided data. 47 | /// 48 | /// - parameter data: The data from which an image should be deserialized. 49 | /// - parameter options: Options for deserialization. 50 | /// 51 | /// - returns: An image deserialized or `nil` when no valid image 52 | /// could be deserialized. 53 | func image(with data: Data, options: KingfisherOptionsInfo?) -> Image? 54 | } 55 | 56 | 57 | /// `DefaultCacheSerializer` is a basic `CacheSerializer` used in default cache of 58 | /// Kingfisher. It could serialize and deserialize PNG, JEPG and GIF images. For 59 | /// image other than these formats, a normalized `pngRepresentation` will be used. 60 | public struct DefaultCacheSerializer: CacheSerializer { 61 | 62 | public static let `default` = DefaultCacheSerializer() 63 | private init() {} 64 | 65 | public func data(with image: Image, original: Data?) -> Data? { 66 | let imageFormat = original?.kf.imageFormat ?? .unknown 67 | 68 | let data: Data? 69 | switch imageFormat { 70 | case .PNG: data = image.kf.pngRepresentation() 71 | case .JPEG: data = image.kf.jpegRepresentation(compressionQuality: 1.0) 72 | case .GIF: data = image.kf.gifRepresentation() 73 | case .unknown: data = original ?? image.kf.normalized.kf.pngRepresentation() 74 | } 75 | 76 | return data 77 | } 78 | 79 | public func image(with data: Data, options: KingfisherOptionsInfo?) -> Image? { 80 | let options = options ?? KingfisherEmptyOptionsInfo 81 | return Kingfisher.image( 82 | data: data, 83 | scale: options.scaleFactor, 84 | preloadAllGIFData: options.preloadAllGIFData, 85 | onlyFirstFrame: options.onlyLoadFirstFrame) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/Filter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Filter.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 2016/08/31. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | 28 | 29 | import CoreImage 30 | import Accelerate 31 | 32 | // Reuse the same CI Context for all CI drawing. 33 | private let ciContext = CIContext(options: nil) 34 | 35 | /// Transformer method which will be used in to provide a `Filter`. 36 | public typealias Transformer = (CIImage) -> CIImage? 37 | 38 | /// Supply a filter to create an `ImageProcessor`. 39 | public protocol CIImageProcessor: ImageProcessor { 40 | var filter: Filter { get } 41 | } 42 | 43 | extension CIImageProcessor { 44 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 45 | switch item { 46 | case .image(let image): 47 | return image.kf.apply(filter) 48 | case .data(_): 49 | return (DefaultImageProcessor.default >> self).process(item: item, options: options) 50 | } 51 | } 52 | } 53 | 54 | /// Wrapper for a `Transformer` of CIImage filters. 55 | public struct Filter { 56 | 57 | let transform: Transformer 58 | 59 | public init(tranform: @escaping Transformer) { 60 | self.transform = tranform 61 | } 62 | 63 | /// Tint filter which will apply a tint color to images. 64 | public static var tint: (Color) -> Filter = { 65 | color in 66 | Filter { input in 67 | let colorFilter = CIFilter(name: "CIConstantColorGenerator")! 68 | colorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey) 69 | 70 | let colorImage = colorFilter.outputImage 71 | let filter = CIFilter(name: "CISourceOverCompositing")! 72 | filter.setValue(colorImage, forKey: kCIInputImageKey) 73 | filter.setValue(input, forKey: kCIInputBackgroundImageKey) 74 | return filter.outputImage?.cropping(to: input.extent) 75 | } 76 | } 77 | 78 | public typealias ColorElement = (CGFloat, CGFloat, CGFloat, CGFloat) 79 | 80 | /// Color control filter which will apply color control change to images. 81 | public static var colorControl: (ColorElement) -> Filter = { 82 | brightness, contrast, saturation, inputEV in 83 | Filter { input in 84 | let paramsColor = [kCIInputBrightnessKey: brightness, 85 | kCIInputContrastKey: contrast, 86 | kCIInputSaturationKey: saturation] 87 | 88 | let blackAndWhite = input.applyingFilter("CIColorControls", withInputParameters: paramsColor) 89 | let paramsExposure = [kCIInputEVKey: inputEV] 90 | return blackAndWhite.applyingFilter("CIExposureAdjust", withInputParameters: paramsExposure) 91 | } 92 | 93 | } 94 | } 95 | 96 | extension Kingfisher where Base: Image { 97 | /// Apply a `Filter` containing `CIImage` transformer to `self`. 98 | /// 99 | /// - parameter filter: The filter used to transform `self`. 100 | /// 101 | /// - returns: A transformed image by input `Filter`. 102 | /// 103 | /// - Note: Only CG-based images are supported. If any error happens during transforming, `self` will be returned. 104 | public func apply(_ filter: Filter) -> Image { 105 | 106 | guard let cgImage = cgImage else { 107 | assertionFailure("[Kingfisher] Tint image only works for CG-based image.") 108 | return base 109 | } 110 | 111 | let inputImage = CIImage(cgImage: cgImage) 112 | guard let outputImage = filter.transform(inputImage) else { 113 | return base 114 | } 115 | 116 | guard let result = ciContext.createCGImage(outputImage, from: outputImage.extent) else { 117 | assertionFailure("[Kingfisher] Can not make an tint image within context.") 118 | return base 119 | } 120 | 121 | #if os(macOS) 122 | return fixedForRetinaPixel(cgImage: result, to: size) 123 | #else 124 | return Image(cgImage: result, scale: base.scale, orientation: base.imageOrientation) 125 | #endif 126 | } 127 | 128 | } 129 | 130 | public extension Image { 131 | 132 | /// Apply a `Filter` containing `CIImage` transformer to `self`. 133 | /// 134 | /// - parameter filter: The filter used to transform `self`. 135 | /// 136 | /// - returns: A transformed image by input `Filter`. 137 | /// 138 | /// - Note: Only CG-based images are supported. If any error happens during transforming, `self` will be returned. 139 | @available(*, deprecated, 140 | message: "Extensions directly on Image are deprecated. Use `kf.apply` instead.", 141 | renamed: "kf.apply") 142 | public func kf_apply(_ filter: Filter) -> Image { 143 | return kf.apply(filter) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/ImagePrefetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePrefetcher.swift 3 | // Kingfisher 4 | // 5 | // Created by Claire Knight on 24/02/2016 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | 28 | #if os(macOS) 29 | import AppKit 30 | #else 31 | import UIKit 32 | #endif 33 | 34 | 35 | /// Progress update block of prefetcher. 36 | /// 37 | /// - `skippedResources`: An array of resources that are already cached before the prefetching starting. 38 | /// - `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. 39 | /// - `completedResources`: An array of resources that are downloaded and cached successfully. 40 | public typealias PrefetcherProgressBlock = ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> ()) 41 | 42 | /// Completion block of prefetcher. 43 | /// 44 | /// - `skippedResources`: An array of resources that are already cached before the prefetching starting. 45 | /// - `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. 46 | /// - `completedResources`: An array of resources that are downloaded and cached successfully. 47 | public typealias PrefetcherCompletionHandler = ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> ()) 48 | 49 | /// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them. 50 | /// This is useful when you know a list of image resources and want to download them before showing. 51 | public class ImagePrefetcher { 52 | 53 | /// The maximum concurrent downloads to use when prefetching images. Default is 5. 54 | public var maxConcurrentDownloads = 5 55 | 56 | private let prefetchResources: [Resource] 57 | private let optionsInfo: KingfisherOptionsInfo 58 | private var progressBlock: PrefetcherProgressBlock? 59 | private var completionHandler: PrefetcherCompletionHandler? 60 | 61 | private var tasks = [URL: RetrieveImageDownloadTask]() 62 | 63 | private var pendingResources: ArraySlice 64 | private var skippedResources = [Resource]() 65 | private var completedResources = [Resource]() 66 | private var failedResources = [Resource]() 67 | 68 | private var stopped = false 69 | 70 | // The created manager used for prefetch. We will use the helper method in manager. 71 | private let manager: KingfisherManager 72 | 73 | private var finished: Bool { 74 | return failedResources.count + skippedResources.count + completedResources.count == prefetchResources.count && self.tasks.isEmpty 75 | } 76 | 77 | /** 78 | Init an image prefetcher with an array of URLs. 79 | 80 | The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. 81 | After you get a valid `ImagePrefetcher` object, you could call `start()` on it to begin the prefetching process. 82 | The images already cached will be skipped without downloading again. 83 | 84 | - parameter urls: The URLs which should be prefetched. 85 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 86 | - parameter progressBlock: Called every time an resource is downloaded, skipped or cancelled. 87 | - parameter completionHandler: Called when the whole prefetching process finished. 88 | 89 | - returns: An `ImagePrefetcher` object. 90 | 91 | - Note: By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as 92 | the downloader and cache target respectively. You can specify another downloader or cache by using a customized `KingfisherOptionsInfo`. 93 | Both the progress and completion block will be invoked in main thread. The `CallbackDispatchQueue` in `optionsInfo` will be ignored in this method. 94 | */ 95 | public convenience init(urls: [URL], 96 | options: KingfisherOptionsInfo? = nil, 97 | progressBlock: PrefetcherProgressBlock? = nil, 98 | completionHandler: PrefetcherCompletionHandler? = nil) 99 | { 100 | let resources: [Resource] = urls.map { $0 } 101 | self.init(resources: resources, options: options, progressBlock: progressBlock, completionHandler: completionHandler) 102 | } 103 | 104 | /** 105 | Init an image prefetcher with an array of resources. 106 | 107 | The prefetcher should be initiated with a list of prefetching targets. The resources list is immutable. 108 | After you get a valid `ImagePrefetcher` object, you could call `start()` on it to begin the prefetching process. 109 | The images already cached will be skipped without downloading again. 110 | 111 | - parameter resources: The resources which should be prefetched. See `Resource` type for more. 112 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 113 | - parameter progressBlock: Called every time an resource is downloaded, skipped or cancelled. 114 | - parameter completionHandler: Called when the whole prefetching process finished. 115 | 116 | - returns: An `ImagePrefetcher` object. 117 | 118 | - Note: By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as 119 | the downloader and cache target respectively. You can specify another downloader or cache by using a customized `KingfisherOptionsInfo`. 120 | Both the progress and completion block will be invoked in main thread. The `CallbackDispatchQueue` in `optionsInfo` will be ignored in this method. 121 | */ 122 | public init(resources: [Resource], 123 | options: KingfisherOptionsInfo? = nil, 124 | progressBlock: PrefetcherProgressBlock? = nil, 125 | completionHandler: PrefetcherCompletionHandler? = nil) 126 | { 127 | prefetchResources = resources 128 | pendingResources = ArraySlice(resources) 129 | 130 | // We want all callbacks from main queue, so we ignore the call back queue in options 131 | let optionsInfoWithoutQueue = options?.removeAllMatchesIgnoringAssociatedValue(.callbackDispatchQueue(nil)) 132 | self.optionsInfo = optionsInfoWithoutQueue ?? KingfisherEmptyOptionsInfo 133 | 134 | let cache = self.optionsInfo.targetCache 135 | let downloader = self.optionsInfo.downloader 136 | manager = KingfisherManager(downloader: downloader, cache: cache) 137 | 138 | self.progressBlock = progressBlock 139 | self.completionHandler = completionHandler 140 | } 141 | 142 | /** 143 | Start to download the resources and cache them. This can be useful for background downloading 144 | of assets that are required for later use in an app. This code will not try and update any UI 145 | with the results of the process. 146 | */ 147 | public func start() 148 | { 149 | // Since we want to handle the resources cancellation in main thread only. 150 | DispatchQueue.main.safeAsync { 151 | 152 | guard !self.stopped else { 153 | assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.") 154 | self.handleComplete() 155 | return 156 | } 157 | 158 | guard self.maxConcurrentDownloads > 0 else { 159 | assertionFailure("There should be concurrent downloads value should be at least 1.") 160 | self.handleComplete() 161 | return 162 | } 163 | 164 | guard self.prefetchResources.count > 0 else { 165 | self.handleComplete() 166 | return 167 | } 168 | 169 | let initialConcurentDownloads = min(self.prefetchResources.count, self.maxConcurrentDownloads) 170 | for _ in 0 ..< initialConcurentDownloads { 171 | if let resource = self.pendingResources.popFirst() { 172 | self.startPrefetching(resource) 173 | } 174 | } 175 | } 176 | } 177 | 178 | 179 | /** 180 | Stop current downloading progress, and cancel any future prefetching activity that might be occuring. 181 | */ 182 | public func stop() { 183 | DispatchQueue.main.safeAsync { 184 | 185 | if self.finished { return } 186 | 187 | self.stopped = true 188 | self.tasks.forEach { (_, task) -> () in 189 | task.cancel() 190 | } 191 | } 192 | } 193 | 194 | func downloadAndCache(_ resource: Resource) { 195 | 196 | let downloadTaskCompletionHandler: CompletionHandler = { (image, error, _, _) -> () in 197 | self.tasks.removeValue(forKey: resource.downloadURL) 198 | if let _ = error { 199 | self.failedResources.append(resource) 200 | } else { 201 | self.completedResources.append(resource) 202 | } 203 | 204 | self.reportProgress() 205 | if self.stopped { 206 | if self.tasks.isEmpty { 207 | self.failedResources.append(contentsOf: self.pendingResources) 208 | self.handleComplete() 209 | } 210 | } else { 211 | self.reportCompletionOrStartNext() 212 | } 213 | } 214 | 215 | let downloadTask = manager.downloadAndCacheImage( 216 | with: resource.downloadURL, 217 | forKey: resource.cacheKey, 218 | retrieveImageTask: RetrieveImageTask(), 219 | progressBlock: nil, 220 | completionHandler: downloadTaskCompletionHandler, 221 | options: optionsInfo) 222 | 223 | if let downloadTask = downloadTask { 224 | tasks[resource.downloadURL] = downloadTask 225 | } 226 | } 227 | 228 | func append(cached resource: Resource) { 229 | skippedResources.append(resource) 230 | 231 | reportProgress() 232 | reportCompletionOrStartNext() 233 | } 234 | 235 | func startPrefetching(_ resource: Resource) 236 | { 237 | if optionsInfo.forceRefresh { 238 | downloadAndCache(resource) 239 | } else { 240 | let alreadyInCache = manager.cache.isImageCached(forKey: resource.cacheKey, 241 | processorIdentifier: optionsInfo.processor.identifier).cached 242 | 243 | if alreadyInCache { 244 | append(cached: resource) 245 | } else { 246 | downloadAndCache(resource) 247 | } 248 | } 249 | } 250 | 251 | func reportProgress() { 252 | progressBlock?(skippedResources, failedResources, completedResources) 253 | } 254 | 255 | func reportCompletionOrStartNext() { 256 | if let resource = pendingResources.popFirst() { 257 | startPrefetching(resource) 258 | } else { 259 | guard tasks.isEmpty else { return } 260 | handleComplete() 261 | } 262 | } 263 | 264 | func handleComplete() { 265 | completionHandler?(skippedResources, failedResources, completedResources) 266 | completionHandler = nil 267 | progressBlock = nil 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/ImageProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageProcessor.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 2016/08/26. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | import CoreGraphics 29 | 30 | 31 | /// The item which could be processed by an `ImageProcessor` 32 | /// 33 | /// - image: Input image 34 | /// - data: Input data 35 | public enum ImageProcessItem { 36 | case image(Image) 37 | case data(Data) 38 | } 39 | 40 | /// An `ImageProcessor` would be used to convert some downloaded data to an image. 41 | public protocol ImageProcessor { 42 | /// Identifier of the processor. It will be used to identify the processor when 43 | /// caching and retriving an image. You might want to make sure that processors with 44 | /// same properties/functionality have the same identifiers, so correct processed images 45 | /// could be retrived with proper key. 46 | /// 47 | /// - Note: Do not supply an empty string for a customized processor, which is already taken by 48 | /// the `DefaultImageProcessor`. It is recommended to use a reverse domain name notation 49 | /// string of your own for the identifier. 50 | var identifier: String { get } 51 | 52 | /// Process an input `ImageProcessItem` item to an image for this processor. 53 | /// 54 | /// - parameter item: Input item which will be processed by `self` 55 | /// - parameter options: Options when processing the item. 56 | /// 57 | /// - returns: The processed image. 58 | /// 59 | /// - Note: The return value will be `nil` if processing failed while converting data to image. 60 | /// If input item is already an image and there is any errors in processing, the input 61 | /// image itself will be returned. 62 | /// - Note: Most processor only supports CG-based images. 63 | /// watchOS is not supported for processers containing filter, the input image will be returned directly on watchOS. 64 | func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? 65 | } 66 | 67 | typealias ProcessorImp = ((ImageProcessItem, KingfisherOptionsInfo) -> Image?) 68 | 69 | public extension ImageProcessor { 70 | 71 | /// Append an `ImageProcessor` to another. The identifier of the new `ImageProcessor` 72 | /// will be "\(self.identifier)|>\(another.identifier)". 73 | /// 74 | /// - parameter another: An `ImageProcessor` you want to append to `self`. 75 | /// 76 | /// - returns: The new `ImageProcessor`. It will process the image in the order 77 | /// of the two processors concatenated. 78 | public func append(another: ImageProcessor) -> ImageProcessor { 79 | let newIdentifier = identifier.appending("|>\(another.identifier)") 80 | return GeneralProcessor(identifier: newIdentifier) { 81 | item, options in 82 | if let image = self.process(item: item, options: options) { 83 | return another.process(item: .image(image), options: options) 84 | } else { 85 | return nil 86 | } 87 | } 88 | } 89 | } 90 | 91 | fileprivate struct GeneralProcessor: ImageProcessor { 92 | let identifier: String 93 | let p: ProcessorImp 94 | func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 95 | return p(item, options) 96 | } 97 | } 98 | 99 | /// The default processor. It convert the input data to a valid image. 100 | /// Images of .PNG, .JPEG and .GIF format are supported. 101 | /// If an image is given, `DefaultImageProcessor` will do nothing on it and just return that image. 102 | public struct DefaultImageProcessor: ImageProcessor { 103 | 104 | /// A default `DefaultImageProcessor` could be used across. 105 | public static let `default` = DefaultImageProcessor() 106 | 107 | public let identifier = "" 108 | 109 | /// Initialize a `DefaultImageProcessor` 110 | /// 111 | /// - returns: An initialized `DefaultImageProcessor`. 112 | public init() {} 113 | 114 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 115 | switch item { 116 | case .image(let image): 117 | return image 118 | case .data(let data): 119 | return Kingfisher.image( 120 | data: data, 121 | scale: options.scaleFactor, 122 | preloadAllGIFData: options.preloadAllGIFData, 123 | onlyFirstFrame: options.onlyLoadFirstFrame) 124 | } 125 | } 126 | } 127 | 128 | /// Processor for making round corner images. Only CG-based images are supported in macOS, 129 | /// if a non-CG image passed in, the processor will do nothing. 130 | public struct RoundCornerImageProcessor: ImageProcessor { 131 | public let identifier: String 132 | 133 | /// Corner radius will be applied in processing. 134 | public let cornerRadius: CGFloat 135 | 136 | /// Target size of output image should be. If `nil`, the image will keep its original size after processing. 137 | public let targetSize: CGSize? 138 | 139 | /// Initialize a `RoundCornerImageProcessor` 140 | /// 141 | /// - parameter cornerRadius: Corner radius will be applied in processing. 142 | /// - parameter targetSize: Target size of output image should be. If `nil`, 143 | /// the image will keep its original size after processing. 144 | /// Default is `nil`. 145 | /// 146 | /// - returns: An initialized `RoundCornerImageProcessor`. 147 | public init(cornerRadius: CGFloat, targetSize: CGSize? = nil) { 148 | self.cornerRadius = cornerRadius 149 | self.targetSize = targetSize 150 | if let size = targetSize { 151 | self.identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(cornerRadius)_\(size))" 152 | } else { 153 | self.identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(cornerRadius))" 154 | } 155 | } 156 | 157 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 158 | switch item { 159 | case .image(let image): 160 | let size = targetSize ?? image.kf.size 161 | return image.kf.image(withRoundRadius: cornerRadius, fit: size) 162 | case .data(_): 163 | return (DefaultImageProcessor.default >> self).process(item: item, options: options) 164 | } 165 | } 166 | } 167 | 168 | 169 | /// Specify how a size adjusts itself to fit a target. 170 | /// 171 | /// - none: Not scale the content. 172 | /// - aspectFit: Scale the content to fit the size of the view by maintaining the aspect ratio. 173 | /// - aspectFill: Scale the content to fill the size of the view 174 | public enum ContentMode { 175 | case none 176 | case aspectFit 177 | case aspectFill 178 | } 179 | 180 | /// Processor for resizing images. Only CG-based images are supported in macOS. 181 | public struct ResizingImageProcessor: ImageProcessor { 182 | public let identifier: String 183 | 184 | /// Target size of output image should be. 185 | public let targetSize: CGSize 186 | 187 | /// Target content mode of output image should be. 188 | /// Default to ContentMode.none 189 | public let targetContentMode: ContentMode 190 | 191 | /// Initialize a `ResizingImageProcessor` 192 | /// 193 | /// - parameter targetSize: Target size of output image should be. 194 | /// - parameter contentMode: Target content mode of output image should be. 195 | /// 196 | /// - returns: An initialized `ResizingImageProcessor`. 197 | public init(targetSize: CGSize, contentMode: ContentMode = .none) { 198 | self.targetSize = targetSize 199 | self.targetContentMode = contentMode 200 | 201 | if contentMode == .none { 202 | self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(targetSize))" 203 | } else { 204 | self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(targetSize), \(contentMode))" 205 | } 206 | } 207 | 208 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 209 | switch item { 210 | case .image(let image): 211 | return image.kf.resize(to: targetSize, for: targetContentMode) 212 | case .data(_): 213 | return (DefaultImageProcessor.default >> self).process(item: item, options: options) 214 | } 215 | } 216 | } 217 | 218 | /// Processor for adding blur effect to images. `Accelerate.framework` is used underhood for 219 | /// a better performance. A simulated Gaussian blur with specified blur radius will be applied. 220 | public struct BlurImageProcessor: ImageProcessor { 221 | public let identifier: String 222 | 223 | /// Blur radius for the simulated Gaussian blur. 224 | public let blurRadius: CGFloat 225 | 226 | /// Initialize a `BlurImageProcessor` 227 | /// 228 | /// - parameter blurRadius: Blur radius for the simulated Gaussian blur. 229 | /// 230 | /// - returns: An initialized `BlurImageProcessor`. 231 | public init(blurRadius: CGFloat) { 232 | self.blurRadius = blurRadius 233 | self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))" 234 | } 235 | 236 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 237 | switch item { 238 | case .image(let image): 239 | let radius = blurRadius * options.scaleFactor 240 | return image.kf.blurred(withRadius: radius) 241 | case .data(_): 242 | return (DefaultImageProcessor.default >> self).process(item: item, options: options) 243 | } 244 | } 245 | } 246 | 247 | /// Processor for adding an overlay to images. Only CG-based images are supported in macOS. 248 | public struct OverlayImageProcessor: ImageProcessor { 249 | 250 | public var identifier: String 251 | 252 | /// Overlay color will be used to overlay the input image. 253 | public let overlay: Color 254 | 255 | /// Fraction will be used when overlay the color to image. 256 | public let fraction: CGFloat 257 | 258 | /// Initialize an `OverlayImageProcessor` 259 | /// 260 | /// - parameter overlay: Overlay color will be used to overlay the input image. 261 | /// - parameter fraction: Fraction will be used when overlay the color to image. 262 | /// From 0.0 to 1.0. 0.0 means solid color, 1.0 means transparent overlay. 263 | /// 264 | /// - returns: An initialized `OverlayImageProcessor`. 265 | public init(overlay: Color, fraction: CGFloat = 0.5) { 266 | self.overlay = overlay 267 | self.fraction = fraction 268 | self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.hex)_\(fraction))" 269 | } 270 | 271 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 272 | switch item { 273 | case .image(let image): 274 | return image.kf.overlaying(with: overlay, fraction: fraction) 275 | case .data(_): 276 | return (DefaultImageProcessor.default >> self).process(item: item, options: options) 277 | } 278 | } 279 | } 280 | 281 | /// Processor for tint images with color. Only CG-based images are supported. 282 | public struct TintImageProcessor: ImageProcessor { 283 | 284 | public let identifier: String 285 | 286 | /// Tint color will be used to tint the input image. 287 | public let tint: Color 288 | 289 | /// Initialize a `TintImageProcessor` 290 | /// 291 | /// - parameter tint: Tint color will be used to tint the input image. 292 | /// 293 | /// - returns: An initialized `TintImageProcessor`. 294 | public init(tint: Color) { 295 | self.tint = tint 296 | self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.hex))" 297 | } 298 | 299 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 300 | switch item { 301 | case .image(let image): 302 | return image.kf.tinted(with: tint) 303 | case .data(_): 304 | return (DefaultImageProcessor.default >> self).process(item: item, options: options) 305 | } 306 | } 307 | } 308 | 309 | /// Processor for applying some color control to images. Only CG-based images are supported. 310 | /// watchOS is not supported. 311 | public struct ColorControlsProcessor: ImageProcessor { 312 | 313 | public let identifier: String 314 | 315 | /// Brightness changing to image. 316 | public let brightness: CGFloat 317 | 318 | /// Contrast changing to image. 319 | public let contrast: CGFloat 320 | 321 | /// Saturation changing to image. 322 | public let saturation: CGFloat 323 | 324 | /// InputEV changing to image. 325 | public let inputEV: CGFloat 326 | 327 | /// Initialize a `ColorControlsProcessor` 328 | /// 329 | /// - parameter brightness: Brightness changing to image. 330 | /// - parameter contrast: Contrast changing to image. 331 | /// - parameter saturation: Saturation changing to image. 332 | /// - parameter inputEV: InputEV changing to image. 333 | /// 334 | /// - returns: An initialized `ColorControlsProcessor` 335 | public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) { 336 | self.brightness = brightness 337 | self.contrast = contrast 338 | self.saturation = saturation 339 | self.inputEV = inputEV 340 | self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))" 341 | } 342 | 343 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 344 | switch item { 345 | case .image(let image): 346 | return image.kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV) 347 | case .data(_): 348 | return (DefaultImageProcessor.default >> self).process(item: item, options: options) 349 | } 350 | } 351 | } 352 | 353 | /// Processor for applying black and white effect to images. Only CG-based images are supported. 354 | /// watchOS is not supported. 355 | public struct BlackWhiteProcessor: ImageProcessor { 356 | public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor" 357 | 358 | /// Initialize a `BlackWhiteProcessor` 359 | /// 360 | /// - returns: An initialized `BlackWhiteProcessor` 361 | public init() {} 362 | 363 | public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { 364 | return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7) 365 | .process(item: item, options: options) 366 | } 367 | } 368 | 369 | /// Concatenate two `ImageProcessor`s. `ImageProcessor.appen(another:)` is used internally. 370 | /// 371 | /// - parameter left: First processor. 372 | /// - parameter right: Second processor. 373 | /// 374 | /// - returns: The concatenated processor. 375 | public func >>(left: ImageProcessor, right: ImageProcessor) -> ImageProcessor { 376 | return left.append(another: right) 377 | } 378 | 379 | fileprivate extension Color { 380 | var hex: String { 381 | var r: CGFloat = 0 382 | var g: CGFloat = 0 383 | var b: CGFloat = 0 384 | var a: CGFloat = 0 385 | 386 | getRed(&r, green: &g, blue: &b, alpha: &a) 387 | 388 | let rInt = Int(r * 255) << 24 389 | let gInt = Int(g * 255) << 16 390 | let bInt = Int(b * 255) << 8 391 | let aInt = Int(a * 255) 392 | 393 | let rgba = rInt | gInt | bInt | aInt 394 | 395 | return String(format:"#%08x", rgba) 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/ImageTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageTransition.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 15/9/18. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #if os(macOS) 28 | // Not implemented for macOS and watchOS yet. 29 | 30 | import AppKit 31 | 32 | /// Image transition is not supported on macOS. 33 | public enum ImageTransition { 34 | case none 35 | var duration: TimeInterval { 36 | return 0 37 | } 38 | } 39 | 40 | #elseif os(watchOS) 41 | import UIKit 42 | /// Image transition is not supported on watchOS. 43 | public enum ImageTransition { 44 | case none 45 | var duration: TimeInterval { 46 | return 0 47 | } 48 | } 49 | #else 50 | import UIKit 51 | 52 | /** 53 | Transition effect which will be used when an image downloaded and set by `UIImageView` extension API in Kingfisher. 54 | You can assign an enum value with transition duration as an item in `KingfisherOptionsInfo` 55 | to enable the animation transition. 56 | 57 | Apple's UIViewAnimationOptions is used under the hood. 58 | For custom transition, you should specified your own transition options, animations and 59 | comletion handler as well. 60 | */ 61 | public enum ImageTransition { 62 | /// No animation transistion. 63 | case none 64 | 65 | /// Fade in the loaded image. 66 | case fade(TimeInterval) 67 | 68 | /// Flip from left transition. 69 | case flipFromLeft(TimeInterval) 70 | 71 | /// Flip from right transition. 72 | case flipFromRight(TimeInterval) 73 | 74 | /// Flip from top transition. 75 | case flipFromTop(TimeInterval) 76 | 77 | /// Flip from bottom transition. 78 | case flipFromBottom(TimeInterval) 79 | 80 | /// Custom transition. 81 | case custom(duration: TimeInterval, 82 | options: UIViewAnimationOptions, 83 | animations: ((UIImageView, UIImage) -> Void)?, 84 | completion: ((Bool) -> Void)?) 85 | 86 | var duration: TimeInterval { 87 | switch self { 88 | case .none: return 0 89 | case .fade(let duration): return duration 90 | 91 | case .flipFromLeft(let duration): return duration 92 | case .flipFromRight(let duration): return duration 93 | case .flipFromTop(let duration): return duration 94 | case .flipFromBottom(let duration): return duration 95 | 96 | case .custom(let duration, _, _, _): return duration 97 | } 98 | } 99 | 100 | var animationOptions: UIViewAnimationOptions { 101 | switch self { 102 | case .none: return [] 103 | case .fade(_): return .transitionCrossDissolve 104 | 105 | case .flipFromLeft(_): return .transitionFlipFromLeft 106 | case .flipFromRight(_): return .transitionFlipFromRight 107 | case .flipFromTop(_): return .transitionFlipFromTop 108 | case .flipFromBottom(_): return .transitionFlipFromBottom 109 | 110 | case .custom(_, let options, _, _): return options 111 | } 112 | } 113 | 114 | var animations: ((UIImageView, UIImage) -> Void)? { 115 | switch self { 116 | case .custom(_, _, let animations, _): return animations 117 | default: return { $0.image = $1 } 118 | } 119 | } 120 | 121 | var completion: ((Bool) -> Void)? { 122 | switch self { 123 | case .custom(_, _, _, let completion): return completion 124 | default: return nil 125 | } 126 | } 127 | } 128 | #endif 129 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/ImageView+Kingfisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageView+Kingfisher.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 15/4/6. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | 28 | #if os(macOS) 29 | import AppKit 30 | #else 31 | import UIKit 32 | #endif 33 | 34 | // MARK: - Extension methods. 35 | /** 36 | * Set image to use from web. 37 | */ 38 | extension Kingfisher where Base: ImageView { 39 | /** 40 | Set an image with a resource, a placeholder image, options, progress handler and completion handler. 41 | 42 | - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. 43 | - parameter placeholder: A placeholder image when retrieving the image at URL. 44 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 45 | - parameter progressBlock: Called when the image downloading progress gets updated. 46 | - parameter completionHandler: Called when the image retrieved and set. 47 | 48 | - returns: A task represents the retrieving process. 49 | 50 | - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. 51 | The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. 52 | */ 53 | @discardableResult 54 | public func setImage(with resource: Resource?, 55 | placeholder: Image? = nil, 56 | options: KingfisherOptionsInfo? = nil, 57 | progressBlock: DownloadProgressBlock? = nil, 58 | completionHandler: CompletionHandler? = nil) -> RetrieveImageTask 59 | { 60 | guard let resource = resource else { 61 | base.image = placeholder 62 | setWebURL(nil) 63 | completionHandler?(nil, nil, .none, nil) 64 | return .empty 65 | } 66 | 67 | var options = options ?? KingfisherEmptyOptionsInfo 68 | 69 | if !options.keepCurrentImageWhileLoading { 70 | base.image = placeholder 71 | } 72 | 73 | let maybeIndicator = indicator 74 | maybeIndicator?.startAnimatingView() 75 | 76 | setWebURL(resource.downloadURL) 77 | 78 | if base.shouldPreloadAllGIF() { 79 | options.append(.preloadAllGIFData) 80 | } 81 | 82 | let task = KingfisherManager.shared.retrieveImage( 83 | with: resource, 84 | options: options, 85 | progressBlock: { receivedSize, totalSize in 86 | guard resource.downloadURL == self.webURL else { 87 | return 88 | } 89 | if let progressBlock = progressBlock { 90 | progressBlock(receivedSize, totalSize) 91 | } 92 | }, 93 | completionHandler: {[weak base] image, error, cacheType, imageURL in 94 | DispatchQueue.main.safeAsync { 95 | guard let strongBase = base, imageURL == self.webURL else { 96 | return 97 | } 98 | self.setImageTask(nil) 99 | guard let image = image else { 100 | maybeIndicator?.stopAnimatingView() 101 | completionHandler?(nil, error, cacheType, imageURL) 102 | return 103 | } 104 | 105 | guard let transitionItem = options.firstMatchIgnoringAssociatedValue(.transition(.none)), 106 | case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else 107 | { 108 | maybeIndicator?.stopAnimatingView() 109 | strongBase.image = image 110 | completionHandler?(image, error, cacheType, imageURL) 111 | return 112 | } 113 | 114 | #if !os(macOS) 115 | UIView.transition(with: strongBase, duration: 0.0, options: [], 116 | animations: { maybeIndicator?.stopAnimatingView() }, 117 | completion: { _ in 118 | UIView.transition(with: strongBase, duration: transition.duration, 119 | options: [transition.animationOptions, .allowUserInteraction], 120 | animations: { 121 | // Set image property in the animation. 122 | transition.animations?(strongBase, image) 123 | }, 124 | completion: { finished in 125 | transition.completion?(finished) 126 | completionHandler?(image, error, cacheType, imageURL) 127 | }) 128 | }) 129 | #endif 130 | } 131 | }) 132 | 133 | setImageTask(task) 134 | 135 | return task 136 | } 137 | 138 | /** 139 | Cancel the image download task bounded to the image view if it is running. 140 | Nothing will happen if the downloading has already finished. 141 | */ 142 | public func cancelDownloadTask() { 143 | imageTask?.cancel() 144 | } 145 | } 146 | 147 | // MARK: - Associated Object 148 | private var lastURLKey: Void? 149 | private var indicatorKey: Void? 150 | private var indicatorTypeKey: Void? 151 | private var imageTaskKey: Void? 152 | 153 | extension Kingfisher where Base: ImageView { 154 | /// Get the image URL binded to this image view. 155 | public var webURL: URL? { 156 | return objc_getAssociatedObject(base, &lastURLKey) as? URL 157 | } 158 | 159 | fileprivate func setWebURL(_ url: URL?) { 160 | objc_setAssociatedObject(base, &lastURLKey, url, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 161 | } 162 | 163 | /// Holds which indicator type is going to be used. 164 | /// Default is .none, means no indicator will be shown. 165 | public var indicatorType: IndicatorType { 166 | get { 167 | let indicator = (objc_getAssociatedObject(base, &indicatorTypeKey) as? Box)?.value 168 | return indicator ?? .none 169 | } 170 | 171 | set { 172 | switch newValue { 173 | case .none: 174 | indicator = nil 175 | case .activity: 176 | indicator = ActivityIndicator() 177 | case .image(let data): 178 | indicator = ImageIndicator(imageData: data) 179 | case .custom(let anIndicator): 180 | indicator = anIndicator 181 | } 182 | 183 | objc_setAssociatedObject(base, &indicatorTypeKey, Box(value: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 184 | } 185 | } 186 | 187 | /// Holds any type that conforms to the protocol `Indicator`. 188 | /// The protocol `Indicator` has a `view` property that will be shown when loading an image. 189 | /// It will be `nil` if `indicatorType` is `.none`. 190 | public fileprivate(set) var indicator: Indicator? { 191 | get { 192 | return (objc_getAssociatedObject(base, &indicatorKey) as? Box)?.value 193 | } 194 | 195 | set { 196 | // Remove previous 197 | if let previousIndicator = indicator { 198 | previousIndicator.view.removeFromSuperview() 199 | } 200 | 201 | // Add new 202 | if var newIndicator = newValue { 203 | newIndicator.view.frame = base.frame 204 | newIndicator.viewCenter = CGPoint(x: base.bounds.midX, y: base.bounds.midY) 205 | newIndicator.view.isHidden = true 206 | base.addSubview(newIndicator.view) 207 | } 208 | 209 | // Save in associated object 210 | objc_setAssociatedObject(base, &indicatorKey, Box(value: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 211 | } 212 | } 213 | 214 | fileprivate var imageTask: RetrieveImageTask? { 215 | return objc_getAssociatedObject(base, &imageTaskKey) as? RetrieveImageTask 216 | } 217 | 218 | fileprivate func setImageTask(_ task: RetrieveImageTask?) { 219 | objc_setAssociatedObject(base, &imageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 220 | } 221 | } 222 | 223 | 224 | // MARK: - Deprecated. Only for back compatibility. 225 | /** 226 | * Set image to use from web. Deprecated. Use `kf` namespacing instead. 227 | */ 228 | extension ImageView { 229 | /** 230 | Set an image with a resource, a placeholder image, options, progress handler and completion handler. 231 | 232 | - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. 233 | - parameter placeholder: A placeholder image when retrieving the image at URL. 234 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 235 | - parameter progressBlock: Called when the image downloading progress gets updated. 236 | - parameter completionHandler: Called when the image retrieved and set. 237 | 238 | - returns: A task represents the retrieving process. 239 | 240 | - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. 241 | The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. 242 | */ 243 | @available(*, deprecated, message: "Extensions directly on image views are deprecated. Use `imageView.kf.setImage` instead.", renamed: "kf.setImage") 244 | @discardableResult 245 | public func kf_setImage(with resource: Resource?, 246 | placeholder: Image? = nil, 247 | options: KingfisherOptionsInfo? = nil, 248 | progressBlock: DownloadProgressBlock? = nil, 249 | completionHandler: CompletionHandler? = nil) -> RetrieveImageTask 250 | { 251 | return kf.setImage(with: resource, placeholder: placeholder, options: options, progressBlock: progressBlock, completionHandler: completionHandler) 252 | } 253 | 254 | /** 255 | Cancel the image download task bounded to the image view if it is running. 256 | Nothing will happen if the downloading has already finished. 257 | */ 258 | @available(*, deprecated, message: "Extensions directly on image views are deprecated. Use `imageView.kf.cancelDownloadTask` instead.", renamed: "kf.cancelDownloadTask") 259 | public func kf_cancelDownloadTask() { kf.cancelDownloadTask() } 260 | 261 | /// Get the image URL binded to this image view. 262 | @available(*, deprecated, message: "Extensions directly on image views are deprecated. Use `imageView.kf.webURL` instead.", renamed: "kf.webURL") 263 | public var kf_webURL: URL? { return kf.webURL } 264 | 265 | /// Holds which indicator type is going to be used. 266 | /// Default is .none, means no indicator will be shown. 267 | @available(*, deprecated, message: "Extensions directly on image views are deprecated. Use `imageView.kf.indicatorType` instead.", renamed: "kf.indicatorType") 268 | public var kf_indicatorType: IndicatorType { 269 | get { return kf.indicatorType } 270 | set { kf.indicatorType = newValue } 271 | } 272 | 273 | @available(*, deprecated, message: "Extensions directly on image views are deprecated. Use `imageView.kf.indicator` instead.", renamed: "kf.indicator") 274 | /// Holds any type that conforms to the protocol `Indicator`. 275 | /// The protocol `Indicator` has a `view` property that will be shown when loading an image. 276 | /// It will be `nil` if `kf_indicatorType` is `.none`. 277 | public private(set) var kf_indicator: Indicator? { 278 | get { return kf.indicator } 279 | set { kf.indicator = newValue } 280 | } 281 | 282 | @available(*, deprecated, message: "Extensions directly on image views are deprecated.", renamed: "kf.imageTask") 283 | fileprivate var kf_imageTask: RetrieveImageTask? { return kf.imageTask } 284 | @available(*, deprecated, message: "Extensions directly on image views are deprecated.", renamed: "kf.setImageTask") 285 | fileprivate func kf_setImageTask(_ task: RetrieveImageTask?) { kf.setImageTask(task) } 286 | @available(*, deprecated, message: "Extensions directly on image views are deprecated.", renamed: "kf.setWebURL") 287 | fileprivate func kf_setWebURL(_ url: URL) { kf.setWebURL(url) } 288 | } 289 | 290 | extension ImageView { 291 | func shouldPreloadAllGIF() -> Bool { return true } 292 | } 293 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/Indicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Indicator.swift 3 | // Kingfisher 4 | // 5 | // Created by João D. Moreira on 30/08/16. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #if os(macOS) 28 | import AppKit 29 | #else 30 | import UIKit 31 | #endif 32 | 33 | #if os(macOS) 34 | public typealias IndicatorView = NSView 35 | #else 36 | public typealias IndicatorView = UIView 37 | #endif 38 | 39 | public enum IndicatorType { 40 | /// No indicator. 41 | case none 42 | /// Use system activity indicator. 43 | case activity 44 | /// Use an image as indicator. GIF is supported. 45 | case image(imageData: Data) 46 | /// Use a custom indicator, which conforms to the `Indicator` protocol. 47 | case custom(indicator: Indicator) 48 | } 49 | 50 | // MARK: - Indicator Protocol 51 | public protocol Indicator { 52 | func startAnimatingView() 53 | func stopAnimatingView() 54 | 55 | var viewCenter: CGPoint { get set } 56 | var view: IndicatorView { get } 57 | } 58 | 59 | extension Indicator { 60 | #if os(macOS) 61 | public var viewCenter: CGPoint { 62 | get { 63 | let frame = view.frame 64 | return CGPoint(x: frame.origin.x + frame.size.width / 2.0, y: frame.origin.y + frame.size.height / 2.0 ) 65 | } 66 | set { 67 | let frame = view.frame 68 | let newFrame = CGRect(x: newValue.x - frame.size.width / 2.0, 69 | y: newValue.y - frame.size.height / 2.0, 70 | width: frame.size.width, 71 | height: frame.size.height) 72 | view.frame = newFrame 73 | } 74 | } 75 | #else 76 | public var viewCenter: CGPoint { 77 | get { 78 | return view.center 79 | } 80 | set { 81 | view.center = newValue 82 | } 83 | } 84 | #endif 85 | } 86 | 87 | // MARK: - ActivityIndicator 88 | // Displays a NSProgressIndicator / UIActivityIndicatorView 89 | struct ActivityIndicator: Indicator { 90 | 91 | #if os(macOS) 92 | private let activityIndicatorView: NSProgressIndicator 93 | #else 94 | private let activityIndicatorView: UIActivityIndicatorView 95 | #endif 96 | 97 | var view: IndicatorView { 98 | return activityIndicatorView 99 | } 100 | 101 | func startAnimatingView() { 102 | #if os(macOS) 103 | activityIndicatorView.startAnimation(nil) 104 | #else 105 | activityIndicatorView.startAnimating() 106 | #endif 107 | activityIndicatorView.isHidden = false 108 | } 109 | 110 | func stopAnimatingView() { 111 | #if os(macOS) 112 | activityIndicatorView.stopAnimation(nil) 113 | #else 114 | activityIndicatorView.stopAnimating() 115 | #endif 116 | activityIndicatorView.isHidden = true 117 | } 118 | 119 | init() { 120 | #if os(macOS) 121 | activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) 122 | activityIndicatorView.controlSize = .small 123 | activityIndicatorView.style = .spinningStyle 124 | #else 125 | #if os(tvOS) 126 | let indicatorStyle = UIActivityIndicatorViewStyle.white 127 | #else 128 | let indicatorStyle = UIActivityIndicatorViewStyle.gray 129 | #endif 130 | activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle:indicatorStyle) 131 | activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin, .flexibleTopMargin] 132 | #endif 133 | } 134 | } 135 | 136 | // MARK: - ImageIndicator 137 | // Displays an ImageView. Supports gif 138 | struct ImageIndicator: Indicator { 139 | private let animatedImageIndicatorView: ImageView 140 | 141 | var view: IndicatorView { 142 | return animatedImageIndicatorView 143 | } 144 | 145 | init?(imageData data: Data, processor: ImageProcessor = DefaultImageProcessor.default, options: KingfisherOptionsInfo = KingfisherEmptyOptionsInfo) { 146 | 147 | var options = options 148 | // Use normal image view to show gif, so we need to preload all gif data. 149 | if !options.preloadAllGIFData { 150 | options.append(.preloadAllGIFData) 151 | } 152 | 153 | guard let image = processor.process(item: .data(data), options: options) else { 154 | return nil 155 | } 156 | 157 | animatedImageIndicatorView = ImageView() 158 | animatedImageIndicatorView.image = image 159 | 160 | #if os(macOS) 161 | // Need for gif to animate on macOS 162 | self.animatedImageIndicatorView.imageScaling = .scaleNone 163 | self.animatedImageIndicatorView.canDrawSubviewsIntoLayer = true 164 | #else 165 | animatedImageIndicatorView.contentMode = .center 166 | 167 | animatedImageIndicatorView.autoresizingMask = [.flexibleLeftMargin, 168 | .flexibleRightMargin, 169 | .flexibleBottomMargin, 170 | .flexibleTopMargin] 171 | #endif 172 | } 173 | 174 | func startAnimatingView() { 175 | #if os(macOS) 176 | animatedImageIndicatorView.animates = true 177 | #else 178 | animatedImageIndicatorView.startAnimating() 179 | #endif 180 | animatedImageIndicatorView.isHidden = false 181 | } 182 | 183 | func stopAnimatingView() { 184 | #if os(macOS) 185 | animatedImageIndicatorView.animates = false 186 | #else 187 | animatedImageIndicatorView.stopAnimating() 188 | #endif 189 | animatedImageIndicatorView.isHidden = true 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/Kingfisher.h: -------------------------------------------------------------------------------- 1 | // 2 | // Kingfisher.h 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 15/4/6. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #import 28 | 29 | //! Project version number for Kingfisher. 30 | FOUNDATION_EXPORT double KingfisherVersionNumber; 31 | 32 | //! Project version string for Kingfisher. 33 | FOUNDATION_EXPORT const unsigned char KingfisherVersionString[]; 34 | 35 | // In this header, you should import all the public headers of your framework using statements like #import 36 | 37 | 38 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/Kingfisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Kingfisher.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 16/9/14. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | import ImageIO 29 | 30 | #if os(macOS) 31 | import AppKit 32 | public typealias Image = NSImage 33 | public typealias Color = NSColor 34 | public typealias ImageView = NSImageView 35 | typealias Button = NSButton 36 | #else 37 | import UIKit 38 | public typealias Image = UIImage 39 | public typealias Color = UIColor 40 | #if !os(watchOS) 41 | public typealias ImageView = UIImageView 42 | typealias Button = UIButton 43 | #endif 44 | #endif 45 | 46 | public final class Kingfisher { 47 | public let base: Base 48 | public init(_ base: Base) { 49 | self.base = base 50 | } 51 | } 52 | 53 | /** 54 | A type that has Kingfisher extensions. 55 | */ 56 | public protocol KingfisherCompatible { 57 | associatedtype CompatibleType 58 | var kf: CompatibleType { get } 59 | } 60 | 61 | public extension KingfisherCompatible { 62 | public var kf: Kingfisher { 63 | get { return Kingfisher(self) } 64 | } 65 | } 66 | 67 | extension Image: KingfisherCompatible { } 68 | #if !os(watchOS) 69 | extension ImageView: KingfisherCompatible { } 70 | extension Button: KingfisherCompatible { } 71 | #endif 72 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/KingfisherManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KingfisherManager.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 15/4/6. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #if os(macOS) 28 | import AppKit 29 | #else 30 | import UIKit 31 | #endif 32 | 33 | public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> ()) 34 | public typealias CompletionHandler = ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> ()) 35 | 36 | /// RetrieveImageTask represents a task of image retrieving process. 37 | /// It contains an async task of getting image from disk and from network. 38 | public class RetrieveImageTask { 39 | 40 | public static let empty = RetrieveImageTask() 41 | 42 | // If task is canceled before the download task started (which means the `downloadTask` is nil), 43 | // the download task should not begin. 44 | var cancelledBeforeDownloadStarting: Bool = false 45 | 46 | /// The disk retrieve task in this image task. Kingfisher will try to look up in cache first. This task represent the cache search task. 47 | @available(*, deprecated, 48 | message: "diskRetrieveTask is not in use anymore. You cannot cancel a disk retrieve task anymore once it started.") 49 | public var diskRetrieveTask: RetrieveImageDiskTask? 50 | 51 | /// The network retrieve task in this image task. 52 | public var downloadTask: RetrieveImageDownloadTask? 53 | 54 | /** 55 | Cancel current task. If this task is already done, do nothing. 56 | */ 57 | public func cancel() { 58 | if let downloadTask = downloadTask { 59 | downloadTask.cancel() 60 | } else { 61 | cancelledBeforeDownloadStarting = true 62 | } 63 | } 64 | } 65 | 66 | /// Error domain of Kingfisher 67 | public let KingfisherErrorDomain = "com.onevcat.Kingfisher.Error" 68 | 69 | /// Main manager class of Kingfisher. It connects Kingfisher downloader and cache. 70 | /// You can use this class to retrieve an image via a specified URL from web or cache. 71 | public class KingfisherManager { 72 | 73 | /// Shared manager used by the extensions across Kingfisher. 74 | public static let shared = KingfisherManager() 75 | 76 | /// Cache used by this manager 77 | public var cache: ImageCache 78 | 79 | /// Downloader used by this manager 80 | public var downloader: ImageDownloader 81 | 82 | convenience init() { 83 | self.init(downloader: .default, cache: .default) 84 | } 85 | 86 | init(downloader: ImageDownloader, cache: ImageCache) { 87 | self.downloader = downloader 88 | self.cache = cache 89 | } 90 | 91 | /** 92 | Get an image with resource. 93 | If KingfisherOptions.None is used as `options`, Kingfisher will seek the image in memory and disk first. 94 | If not found, it will download the image at `resource.downloadURL` and cache it with `resource.cacheKey`. 95 | These default behaviors could be adjusted by passing different options. See `KingfisherOptions` for more. 96 | 97 | - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. 98 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 99 | - parameter progressBlock: Called every time downloaded data changed. This could be used as a progress UI. 100 | - parameter completionHandler: Called when the whole retrieving process finished. 101 | 102 | - returns: A `RetrieveImageTask` task object. You can use this object to cancel the task. 103 | */ 104 | @discardableResult 105 | public func retrieveImage(with resource: Resource, 106 | options: KingfisherOptionsInfo?, 107 | progressBlock: DownloadProgressBlock?, 108 | completionHandler: CompletionHandler?) -> RetrieveImageTask 109 | { 110 | let task = RetrieveImageTask() 111 | 112 | if let options = options, options.forceRefresh { 113 | _ = downloadAndCacheImage( 114 | with: resource.downloadURL, 115 | forKey: resource.cacheKey, 116 | retrieveImageTask: task, 117 | progressBlock: progressBlock, 118 | completionHandler: completionHandler, 119 | options: options) 120 | } else { 121 | tryToRetrieveImageFromCache( 122 | forKey: resource.cacheKey, 123 | with: resource.downloadURL, 124 | retrieveImageTask: task, 125 | progressBlock: progressBlock, 126 | completionHandler: completionHandler, 127 | options: options) 128 | } 129 | 130 | return task 131 | } 132 | 133 | @discardableResult 134 | func downloadAndCacheImage(with url: URL, 135 | forKey key: String, 136 | retrieveImageTask: RetrieveImageTask, 137 | progressBlock: DownloadProgressBlock?, 138 | completionHandler: CompletionHandler?, 139 | options: KingfisherOptionsInfo?) -> RetrieveImageDownloadTask? 140 | { 141 | let options = options ?? KingfisherEmptyOptionsInfo 142 | let downloader = options.downloader 143 | return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options, 144 | progressBlock: { receivedSize, totalSize in 145 | progressBlock?(receivedSize, totalSize) 146 | }, 147 | completionHandler: { image, error, imageURL, originalData in 148 | 149 | let targetCache = options.targetCache 150 | if let error = error, error.code == KingfisherError.notModified.rawValue { 151 | // Not modified. Try to find the image from cache. 152 | // (The image should be in cache. It should be guaranteed by the framework users.) 153 | targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> () in 154 | completionHandler?(cacheImage, nil, cacheType, url) 155 | }) 156 | return 157 | } 158 | 159 | if let image = image, let originalData = originalData { 160 | targetCache.store(image, 161 | original: originalData, 162 | forKey: key, 163 | processorIdentifier:options.processor.identifier, 164 | cacheSerializer: options.cacheSerializer, 165 | toDisk: !options.cacheMemoryOnly, 166 | completionHandler: nil) 167 | } 168 | 169 | completionHandler?(image, error, .none, url) 170 | 171 | }) 172 | } 173 | 174 | func tryToRetrieveImageFromCache(forKey key: String, 175 | with url: URL, 176 | retrieveImageTask: RetrieveImageTask, 177 | progressBlock: DownloadProgressBlock?, 178 | completionHandler: CompletionHandler?, 179 | options: KingfisherOptionsInfo?) 180 | { 181 | let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in 182 | completionHandler?(image, error, cacheType, imageURL) 183 | } 184 | 185 | let targetCache = options?.targetCache ?? cache 186 | targetCache.retrieveImage(forKey: key, options: options, 187 | completionHandler: { image, cacheType in 188 | if image != nil { 189 | diskTaskCompletionHandler(image, nil, cacheType, url) 190 | } else if let options = options, options.onlyFromCache { 191 | let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil) 192 | diskTaskCompletionHandler(nil, error, .none, url) 193 | } else { 194 | self.downloadAndCacheImage( 195 | with: url, 196 | forKey: key, 197 | retrieveImageTask: retrieveImageTask, 198 | progressBlock: progressBlock, 199 | completionHandler: diskTaskCompletionHandler, 200 | options: options) 201 | } 202 | } 203 | ) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/KingfisherOptionsInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KingfisherOptionsInfo.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 15/4/23. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | #if os(macOS) 28 | import AppKit 29 | #else 30 | import UIKit 31 | #endif 32 | 33 | 34 | /** 35 | * KingfisherOptionsInfo is a typealias for [KingfisherOptionsInfoItem]. You can use the enum of option item with value to control some behaviors of Kingfisher. 36 | */ 37 | public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem] 38 | let KingfisherEmptyOptionsInfo = [KingfisherOptionsInfoItem]() 39 | 40 | /** 41 | Items could be added into KingfisherOptionsInfo. 42 | */ 43 | public enum KingfisherOptionsInfoItem { 44 | /// The associated value of this member should be an ImageCache object. Kingfisher will use the specified 45 | /// cache object when handling related operations, including trying to retrieve the cached images and store 46 | /// the downloaded image to it. 47 | case targetCache(ImageCache) 48 | 49 | /// The associated value of this member should be an ImageDownloader object. Kingfisher will use this 50 | /// downloader to download the images. 51 | case downloader(ImageDownloader) 52 | 53 | /// Member for animation transition when using UIImageView. Kingfisher will use the `ImageTransition` of 54 | /// this enum to animate the image in if it is downloaded from web. The transition will not happen when the 55 | /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when 56 | /// the image being retrieved from cache, set `ForceTransition` as well. 57 | case transition(ImageTransition) 58 | 59 | /// Associated `Float` value will be set as the priority of image download task. The value for it should be 60 | /// between 0.0~1.0. If this option not set, the default value (`NSURLSessionTaskPriorityDefault`) will be used. 61 | case downloadPriority(Float) 62 | 63 | /// If set, `Kingfisher` will ignore the cache and try to fire a download task for the resource. 64 | case forceRefresh 65 | 66 | /// If set, setting the image to an image view will happen with transition even when retrieved from cache. 67 | /// See `Transition` option for more. 68 | case forceTransition 69 | 70 | /// If set, `Kingfisher` will only cache the value in memory but not in disk. 71 | case cacheMemoryOnly 72 | 73 | /// If set, `Kingfisher` will only try to retrieve the image from cache not from network. 74 | case onlyFromCache 75 | 76 | /// Decode the image in background thread before using. 77 | case backgroundDecode 78 | 79 | /// The associated value of this member will be used as the target queue of dispatch callbacks when 80 | /// retrieving images from cache. If not set, `Kingfisher` will use main quese for callbacks. 81 | case callbackDispatchQueue(DispatchQueue?) 82 | 83 | /// The associated value of this member will be used as the scale factor when converting retrieved data to an image. 84 | /// It is the image scale, instead of your screen scale. You may need to specify the correct scale when you dealing 85 | /// with 2x or 3x retina images. 86 | case scaleFactor(CGFloat) 87 | 88 | /// Whether all the GIF data should be preloaded. Default it false, which means following frames will be 89 | /// loaded on need. If true, all the GIF data will be loaded and decoded into memory. This option is mainly 90 | /// used for back compatibility internally. You should not set it directly. `AnimatedImageView` will not preload 91 | /// all data, while a normal image view (`UIImageView` or `NSImageView`) will load all data. Choose to use 92 | /// corresponding image view type instead of setting this option. 93 | case preloadAllGIFData 94 | 95 | /// The `ImageDownloadRequestModifier` contained will be used to change the request before it being sent. 96 | /// This is the last chance you can modify the request. You can modify the request for some customizing purpose, 97 | /// such as adding auth token to the header, do basic HTTP auth or something like url mapping. The original request 98 | /// will be sent without any modification by default. 99 | case requestModifier(ImageDownloadRequestModifier) 100 | 101 | /// Processor for processing when the downloading finishes, a processor will convert the downloaded data to an image 102 | /// and/or apply some filter on it. If a cache is connected to the downloader (it happenes when you are using 103 | /// KingfisherManager or the image extension methods), the converted image will also be sent to cache as well as the 104 | /// image view. `DefaultImageProcessor.default` will be used by default. 105 | case processor(ImageProcessor) 106 | 107 | /// Supply an `CacheSerializer` to convert some data to an image object for 108 | /// retrieving from disk cache or vice versa for storing to disk cache. 109 | /// `DefaultCacheSerializer.default` will be used by default. 110 | case cacheSerializer(CacheSerializer) 111 | 112 | /// Keep the existing image while setting another image to an image view. 113 | /// By setting this option, the placeholder image parameter of imageview extension method 114 | /// will be ignored and the current image will be kept while loading or downloading the new image. 115 | case keepCurrentImageWhileLoading 116 | 117 | /// If set, Kingfisher will only load the first frame from a GIF file as a single image. 118 | /// Loading a lot of GIFs may take too much memory. It will be useful when you want to display a 119 | /// static preview of the first frame from a GIF image. 120 | /// This option will be ignored if the target image is not GIF. 121 | case onlyLoadFirstFrame 122 | } 123 | 124 | precedencegroup ItemComparisonPrecedence { 125 | associativity: none 126 | higherThan: LogicalConjunctionPrecedence 127 | } 128 | 129 | infix operator <== : ItemComparisonPrecedence 130 | 131 | // This operator returns true if two `KingfisherOptionsInfoItem` enum is the same, without considering the associated values. 132 | func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Bool { 133 | switch (lhs, rhs) { 134 | case (.targetCache(_), .targetCache(_)): return true 135 | case (.downloader(_), .downloader(_)): return true 136 | case (.transition(_), .transition(_)): return true 137 | case (.downloadPriority(_), .downloadPriority(_)): return true 138 | case (.forceRefresh, .forceRefresh): return true 139 | case (.forceTransition, .forceTransition): return true 140 | case (.cacheMemoryOnly, .cacheMemoryOnly): return true 141 | case (.onlyFromCache, .onlyFromCache): return true 142 | case (.backgroundDecode, .backgroundDecode): return true 143 | case (.callbackDispatchQueue(_), .callbackDispatchQueue(_)): return true 144 | case (.scaleFactor(_), .scaleFactor(_)): return true 145 | case (.preloadAllGIFData, .preloadAllGIFData): return true 146 | case (.requestModifier(_), .requestModifier(_)): return true 147 | case (.processor(_), .processor(_)): return true 148 | case (.cacheSerializer(_), .cacheSerializer(_)): return true 149 | case (.keepCurrentImageWhileLoading, .keepCurrentImageWhileLoading): return true 150 | case (.onlyLoadFirstFrame, .onlyLoadFirstFrame): return true 151 | default: return false 152 | } 153 | } 154 | 155 | extension Collection where Iterator.Element == KingfisherOptionsInfoItem { 156 | func firstMatchIgnoringAssociatedValue(_ target: Iterator.Element) -> Iterator.Element? { 157 | return index { $0 <== target }.flatMap { self[$0] } 158 | } 159 | 160 | func removeAllMatchesIgnoringAssociatedValue(_ target: Iterator.Element) -> [Iterator.Element] { 161 | return self.filter { !($0 <== target) } 162 | } 163 | } 164 | 165 | public extension Collection where Iterator.Element == KingfisherOptionsInfoItem { 166 | /// The target `ImageCache` which is used. 167 | public var targetCache: ImageCache { 168 | if let item = firstMatchIgnoringAssociatedValue(.targetCache(.default)), 169 | case .targetCache(let cache) = item 170 | { 171 | return cache 172 | } 173 | return ImageCache.default 174 | } 175 | 176 | /// The `ImageDownloader` which is specified. 177 | public var downloader: ImageDownloader { 178 | if let item = firstMatchIgnoringAssociatedValue(.downloader(.default)), 179 | case .downloader(let downloader) = item 180 | { 181 | return downloader 182 | } 183 | return ImageDownloader.default 184 | } 185 | 186 | /// Member for animation transition when using UIImageView. 187 | public var transition: ImageTransition { 188 | if let item = firstMatchIgnoringAssociatedValue(.transition(.none)), 189 | case .transition(let transition) = item 190 | { 191 | return transition 192 | } 193 | return ImageTransition.none 194 | } 195 | 196 | /// A `Float` value set as the priority of image download task. The value for it should be 197 | /// between 0.0~1.0. 198 | public var downloadPriority: Float { 199 | if let item = firstMatchIgnoringAssociatedValue(.downloadPriority(0)), 200 | case .downloadPriority(let priority) = item 201 | { 202 | return priority 203 | } 204 | return URLSessionTask.defaultPriority 205 | } 206 | 207 | /// Whether an image will be always downloaded again or not. 208 | public var forceRefresh: Bool { 209 | return contains{ $0 <== .forceRefresh } 210 | } 211 | 212 | /// Whether the transition should always happen or not. 213 | public var forceTransition: Bool { 214 | return contains{ $0 <== .forceTransition } 215 | } 216 | 217 | /// Whether cache the image only in memory or not. 218 | public var cacheMemoryOnly: Bool { 219 | return contains{ $0 <== .cacheMemoryOnly } 220 | } 221 | 222 | /// Whether only load the images from cache or not. 223 | public var onlyFromCache: Bool { 224 | return contains{ $0 <== .onlyFromCache } 225 | } 226 | 227 | /// Whether the image should be decoded in background or not. 228 | public var backgroundDecode: Bool { 229 | return contains{ $0 <== .backgroundDecode } 230 | } 231 | 232 | /// Whether the image data should be all loaded at once if it is a GIF. 233 | public var preloadAllGIFData: Bool { 234 | return contains { $0 <== .preloadAllGIFData } 235 | } 236 | 237 | /// The queue of callbacks should happen from Kingfisher. 238 | public var callbackDispatchQueue: DispatchQueue { 239 | if let item = firstMatchIgnoringAssociatedValue(.callbackDispatchQueue(nil)), 240 | case .callbackDispatchQueue(let queue) = item 241 | { 242 | return queue ?? DispatchQueue.main 243 | } 244 | return DispatchQueue.main 245 | } 246 | 247 | /// The scale factor which should be used for the image. 248 | public var scaleFactor: CGFloat { 249 | if let item = firstMatchIgnoringAssociatedValue(.scaleFactor(0)), 250 | case .scaleFactor(let scale) = item 251 | { 252 | return scale 253 | } 254 | return 1.0 255 | } 256 | 257 | /// The `ImageDownloadRequestModifier` will be used before sending a download request. 258 | public var modifier: ImageDownloadRequestModifier { 259 | if let item = firstMatchIgnoringAssociatedValue(.requestModifier(NoModifier.default)), 260 | case .requestModifier(let modifier) = item 261 | { 262 | return modifier 263 | } 264 | return NoModifier.default 265 | } 266 | 267 | /// `ImageProcessor` for processing when the downloading finishes. 268 | public var processor: ImageProcessor { 269 | if let item = firstMatchIgnoringAssociatedValue(.processor(DefaultImageProcessor.default)), 270 | case .processor(let processor) = item 271 | { 272 | return processor 273 | } 274 | return DefaultImageProcessor.default 275 | } 276 | 277 | /// `CacheSerializer` to convert image to data for storing in cache. 278 | public var cacheSerializer: CacheSerializer { 279 | if let item = firstMatchIgnoringAssociatedValue(.cacheSerializer(DefaultCacheSerializer.default)), 280 | case .cacheSerializer(let cacheSerializer) = item 281 | { 282 | return cacheSerializer 283 | } 284 | return DefaultCacheSerializer.default 285 | } 286 | 287 | /// Keep the existing image while setting another image to an image view. 288 | /// Or the placeholder will be used while downloading. 289 | public var keepCurrentImageWhileLoading: Bool { 290 | return contains { $0 <== .keepCurrentImageWhileLoading } 291 | } 292 | 293 | public var onlyLoadFirstFrame: Bool { 294 | return contains { $0 <== .onlyLoadFirstFrame } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/RequestModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestModifier.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 2016/09/05. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | /// Request modifier of image downloader. 30 | public protocol ImageDownloadRequestModifier { 31 | func modified(for request: URLRequest) -> URLRequest? 32 | } 33 | 34 | struct NoModifier: ImageDownloadRequestModifier { 35 | static let `default` = NoModifier() 36 | private init() {} 37 | func modified(for request: URLRequest) -> URLRequest? { 38 | return request 39 | } 40 | } 41 | 42 | public struct AnyModifier: ImageDownloadRequestModifier { 43 | 44 | let block: (URLRequest) -> URLRequest? 45 | 46 | public func modified(for request: URLRequest) -> URLRequest? { 47 | return block(request) 48 | } 49 | 50 | public init(modify: @escaping (URLRequest) -> URLRequest? ) { 51 | block = modify 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/Resource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resource.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 15/4/6. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | 30 | /// `Resource` protocol defines how to download and cache a resource from network. 31 | public protocol Resource { 32 | /// The key used in cache. 33 | var cacheKey: String { get } 34 | 35 | /// The target image URL. 36 | var downloadURL: URL { get } 37 | } 38 | 39 | /** 40 | ImageResource is a simple combination of `downloadURL` and `cacheKey`. 41 | 42 | When passed to image view set methods, Kingfisher will try to download the target 43 | image from the `downloadURL`, and then store it with the `cacheKey` as the key in cache. 44 | */ 45 | public struct ImageResource: Resource { 46 | /// The key used in cache. 47 | public let cacheKey: String 48 | 49 | /// The target image URL. 50 | public let downloadURL: URL 51 | 52 | /** 53 | Create a resource. 54 | 55 | - parameter downloadURL: The target image URL. 56 | - parameter cacheKey: The cache key. If `nil`, Kingfisher will use the `absoluteString` of `downloadURL` as the key. 57 | 58 | - returns: A resource. 59 | */ 60 | public init(downloadURL: URL, cacheKey: String? = nil) { 61 | self.downloadURL = downloadURL 62 | self.cacheKey = cacheKey ?? downloadURL.absoluteString 63 | } 64 | } 65 | 66 | /** 67 | URL conforms to `Resource` in Kingfisher. 68 | The `absoluteString` of this URL is used as `cacheKey`. And the URL itself will be used as `downloadURL`. 69 | If you need customize the url and/or cache key, use `ImageResource` instead. 70 | */ 71 | extension URL: Resource { 72 | public var cacheKey: String { return absoluteString } 73 | public var downloadURL: URL { return self } 74 | } 75 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/String+MD5.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+MD5.swift 3 | // Kingfisher 4 | // 5 | // To date, adding CommonCrypto to a Swift framework is problematic. See: 6 | // http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework 7 | // We're using a subset and modified version of CryptoSwift as an alternative. 8 | // The following is an altered source version that only includes MD5. The original software can be found at: 9 | // https://github.com/krzyzanowskim/CryptoSwift 10 | // This is the original copyright notice: 11 | 12 | /* 13 | Copyright (C) 2014 Marcin Krzyżanowski 14 | This software is provided 'as-is', without any express or implied warranty. 15 | In no event will the authors be held liable for any damages arising from the use of this software. 16 | Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 17 | - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. 18 | - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 19 | - This notice may not be removed or altered from any source or binary distribution. 20 | */ 21 | 22 | import Foundation 23 | 24 | public struct StringProxy { 25 | fileprivate let base: String 26 | init(proxy: String) { 27 | base = proxy 28 | } 29 | } 30 | 31 | extension String: KingfisherCompatible { 32 | public typealias CompatibleType = StringProxy 33 | public var kf: CompatibleType { 34 | return StringProxy(proxy: self) 35 | } 36 | } 37 | 38 | extension StringProxy { 39 | var md5: String { 40 | if let data = base.data(using: .utf8, allowLossyConversion: true) { 41 | 42 | let message = data.withUnsafeBytes { bytes -> [UInt8] in 43 | return Array(UnsafeBufferPointer(start: bytes, count: data.count)) 44 | } 45 | 46 | let MD5Calculator = MD5(message) 47 | let MD5Data = MD5Calculator.calculate() 48 | 49 | let MD5String = NSMutableString() 50 | for c in MD5Data { 51 | MD5String.appendFormat("%02x", c) 52 | } 53 | return MD5String as String 54 | 55 | } else { 56 | return base 57 | } 58 | } 59 | } 60 | 61 | 62 | /** array of bytes, little-endian representation */ 63 | func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] { 64 | let totalBytes = length ?? (MemoryLayout.size * 8) 65 | 66 | let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) 67 | valuePointer.pointee = value 68 | 69 | let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in 70 | var bytes = [UInt8](repeating: 0, count: totalBytes) 71 | for j in 0...size, totalBytes) { 72 | bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee 73 | } 74 | return bytes 75 | } 76 | 77 | valuePointer.deinitialize() 78 | valuePointer.deallocate(capacity: 1) 79 | 80 | return bytes 81 | } 82 | 83 | extension Int { 84 | /** Array of bytes with optional padding (little-endian) */ 85 | func bytes(_ totalBytes: Int = MemoryLayout.size) -> [UInt8] { 86 | return arrayOfBytes(self, length: totalBytes) 87 | } 88 | 89 | } 90 | 91 | extension NSMutableData { 92 | 93 | /** Convenient way to append bytes */ 94 | func appendBytes(_ arrayOfBytes: [UInt8]) { 95 | append(arrayOfBytes, length: arrayOfBytes.count) 96 | } 97 | 98 | } 99 | 100 | protocol HashProtocol { 101 | var message: Array { get } 102 | 103 | /** Common part for hash calculation. Prepare header data. */ 104 | func prepare(_ len: Int) -> Array 105 | } 106 | 107 | extension HashProtocol { 108 | 109 | func prepare(_ len: Int) -> Array { 110 | var tmpMessage = message 111 | 112 | // Step 1. Append Padding Bits 113 | tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message 114 | 115 | // append "0" bit until message length in bits ≡ 448 (mod 512) 116 | var msgLength = tmpMessage.count 117 | var counter = 0 118 | 119 | while msgLength % len != (len - 8) { 120 | counter += 1 121 | msgLength += 1 122 | } 123 | 124 | tmpMessage += Array(repeating: 0, count: counter) 125 | return tmpMessage 126 | } 127 | } 128 | 129 | func toUInt32Array(_ slice: ArraySlice) -> Array { 130 | var result = Array() 131 | result.reserveCapacity(16) 132 | 133 | for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout.size) { 134 | let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24 135 | let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16 136 | let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8 137 | let d3 = UInt32(slice[idx]) 138 | let val: UInt32 = d0 | d1 | d2 | d3 139 | 140 | result.append(val) 141 | } 142 | return result 143 | } 144 | 145 | struct BytesIterator: IteratorProtocol { 146 | 147 | let chunkSize: Int 148 | let data: [UInt8] 149 | 150 | init(chunkSize: Int, data: [UInt8]) { 151 | self.chunkSize = chunkSize 152 | self.data = data 153 | } 154 | 155 | var offset = 0 156 | 157 | mutating func next() -> ArraySlice? { 158 | let end = min(chunkSize, data.count - offset) 159 | let result = data[offset.. 0 ? result : nil 162 | } 163 | } 164 | 165 | struct BytesSequence: Sequence { 166 | let chunkSize: Int 167 | let data: [UInt8] 168 | 169 | func makeIterator() -> BytesIterator { 170 | return BytesIterator(chunkSize: chunkSize, data: data) 171 | } 172 | } 173 | 174 | func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 { 175 | return ((value << bits) & 0xFFFFFFFF) | (value >> (32 - bits)) 176 | } 177 | 178 | class MD5: HashProtocol { 179 | 180 | static let size = 16 // 128 / 8 181 | let message: [UInt8] 182 | 183 | init (_ message: [UInt8]) { 184 | self.message = message 185 | } 186 | 187 | /** specifies the per-round shift amounts */ 188 | private let shifts: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 189 | 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 190 | 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 191 | 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] 192 | 193 | /** binary integer part of the sines of integers (Radians) */ 194 | private let sines: [UInt32] = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 195 | 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 196 | 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 197 | 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 198 | 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 199 | 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 200 | 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 201 | 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 202 | 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 203 | 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 204 | 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, 205 | 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 206 | 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 207 | 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 208 | 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 209 | 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391] 210 | 211 | private let hashes: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] 212 | 213 | func calculate() -> [UInt8] { 214 | var tmpMessage = prepare(64) 215 | tmpMessage.reserveCapacity(tmpMessage.count + 4) 216 | 217 | // hash values 218 | var hh = hashes 219 | 220 | // Step 2. Append Length a 64-bit representation of lengthInBits 221 | let lengthInBits = (message.count * 8) 222 | let lengthBytes = lengthInBits.bytes(64 / 8) 223 | tmpMessage += lengthBytes.reversed() 224 | 225 | // Process the message in successive 512-bit chunks: 226 | let chunkSizeBytes = 512 / 8 // 64 227 | 228 | for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) { 229 | // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 230 | var M = toUInt32Array(chunk) 231 | assert(M.count == 16, "Invalid array") 232 | 233 | // Initialize hash value for this chunk: 234 | var A: UInt32 = hh[0] 235 | var B: UInt32 = hh[1] 236 | var C: UInt32 = hh[2] 237 | var D: UInt32 = hh[3] 238 | 239 | var dTemp: UInt32 = 0 240 | 241 | // Main loop 242 | for j in 0 ..< sines.count { 243 | var g = 0 244 | var F: UInt32 = 0 245 | 246 | switch j { 247 | case 0...15: 248 | F = (B & C) | ((~B) & D) 249 | g = j 250 | break 251 | case 16...31: 252 | F = (D & B) | (~D & C) 253 | g = (5 * j + 1) % 16 254 | break 255 | case 32...47: 256 | F = B ^ C ^ D 257 | g = (3 * j + 5) % 16 258 | break 259 | case 48...63: 260 | F = C ^ (B | (~D)) 261 | g = (7 * j) % 16 262 | break 263 | default: 264 | break 265 | } 266 | dTemp = D 267 | D = C 268 | C = B 269 | B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j]) 270 | A = dTemp 271 | } 272 | 273 | hh[0] = hh[0] &+ A 274 | hh[1] = hh[1] &+ B 275 | hh[2] = hh[2] &+ C 276 | hh[3] = hh[3] &+ D 277 | } 278 | 279 | var result = [UInt8]() 280 | result.reserveCapacity(hh.count / 4) 281 | 282 | hh.forEach { 283 | let itemLE = $0.littleEndian 284 | let r1 = UInt8(itemLE & 0xff) 285 | let r2 = UInt8((itemLE >> 8) & 0xff) 286 | let r3 = UInt8((itemLE >> 16) & 0xff) 287 | let r4 = UInt8((itemLE >> 24) & 0xff) 288 | result += [r1, r2, r3, r4] 289 | } 290 | return result 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/ThreadHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadHelper.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 15/10/9. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | extension DispatchQueue { 30 | // This method will dispatch the `block` to self. 31 | // If `self` is the main queue, and current thread is main thread, the block 32 | // will be invoked immediately instead of being dispatched. 33 | func safeAsync(_ block: @escaping ()->()) { 34 | if self === DispatchQueue.main && Thread.isMainThread { 35 | block() 36 | } else { 37 | async { block() } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Linkage/Kingfisher/UIButton+Kingfisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+Kingfisher.swift 3 | // Kingfisher 4 | // 5 | // Created by Wei Wang on 15/4/13. 6 | // 7 | // Copyright (c) 2017 Wei Wang 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | // MARK: - Set Images 30 | /** 31 | * Set image to use in button from web for a specified state. 32 | */ 33 | extension Kingfisher where Base: UIButton { 34 | /** 35 | Set an image to use for a specified state with a resource, a placeholder image, options, progress handler and 36 | completion handler. 37 | 38 | - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. 39 | - parameter state: The state that uses the specified image. 40 | - parameter placeholder: A placeholder image when retrieving the image at URL. 41 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 42 | - parameter progressBlock: Called when the image downloading progress gets updated. 43 | - parameter completionHandler: Called when the image retrieved and set. 44 | 45 | - returns: A task represents the retrieving process. 46 | 47 | - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. 48 | The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. 49 | */ 50 | @discardableResult 51 | public func setImage(with resource: Resource?, 52 | for state: UIControlState, 53 | placeholder: UIImage? = nil, 54 | options: KingfisherOptionsInfo? = nil, 55 | progressBlock: DownloadProgressBlock? = nil, 56 | completionHandler: CompletionHandler? = nil) -> RetrieveImageTask 57 | { 58 | guard let resource = resource else { 59 | base.setImage(placeholder, for: state) 60 | setWebURL(nil, for: state) 61 | completionHandler?(nil, nil, .none, nil) 62 | return .empty 63 | } 64 | 65 | let options = options ?? KingfisherEmptyOptionsInfo 66 | if !options.keepCurrentImageWhileLoading { 67 | base.setImage(placeholder, for: state) 68 | } 69 | 70 | setWebURL(resource.downloadURL, for: state) 71 | let task = KingfisherManager.shared.retrieveImage( 72 | with: resource, 73 | options: options, 74 | progressBlock: { receivedSize, totalSize in 75 | guard resource.downloadURL == self.webURL(for: state) else { 76 | return 77 | } 78 | if let progressBlock = progressBlock { 79 | progressBlock(receivedSize, totalSize) 80 | } 81 | }, 82 | completionHandler: {[weak base] image, error, cacheType, imageURL in 83 | DispatchQueue.main.safeAsync { 84 | guard let strongBase = base, imageURL == self.webURL(for: state) else { 85 | return 86 | } 87 | self.setImageTask(nil) 88 | 89 | if image != nil { 90 | strongBase.setImage(image, for: state) 91 | } 92 | 93 | completionHandler?(image, error, cacheType, imageURL) 94 | } 95 | }) 96 | 97 | setImageTask(task) 98 | return task 99 | } 100 | 101 | /** 102 | Cancel the image download task bounded to the image view if it is running. 103 | Nothing will happen if the downloading has already finished. 104 | */ 105 | public func cancelImageDownloadTask() { 106 | imageTask?.cancel() 107 | } 108 | 109 | /** 110 | Set the background image to use for a specified state with a resource, 111 | a placeholder image, options progress handler and completion handler. 112 | 113 | - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. 114 | - parameter state: The state that uses the specified image. 115 | - parameter placeholder: A placeholder image when retrieving the image at URL. 116 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 117 | - parameter progressBlock: Called when the image downloading progress gets updated. 118 | - parameter completionHandler: Called when the image retrieved and set. 119 | 120 | - returns: A task represents the retrieving process. 121 | 122 | - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. 123 | The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. 124 | */ 125 | @discardableResult 126 | public func setBackgroundImage(with resource: Resource?, 127 | for state: UIControlState, 128 | placeholder: UIImage? = nil, 129 | options: KingfisherOptionsInfo? = nil, 130 | progressBlock: DownloadProgressBlock? = nil, 131 | completionHandler: CompletionHandler? = nil) -> RetrieveImageTask 132 | { 133 | guard let resource = resource else { 134 | base.setBackgroundImage(placeholder, for: state) 135 | setBackgroundWebURL(nil, for: state) 136 | completionHandler?(nil, nil, .none, nil) 137 | return .empty 138 | } 139 | 140 | let options = options ?? KingfisherEmptyOptionsInfo 141 | if !options.keepCurrentImageWhileLoading { 142 | base.setBackgroundImage(placeholder, for: state) 143 | } 144 | 145 | setBackgroundWebURL(resource.downloadURL, for: state) 146 | let task = KingfisherManager.shared.retrieveImage( 147 | with: resource, 148 | options: options, 149 | progressBlock: { receivedSize, totalSize in 150 | guard resource.downloadURL == self.backgroundWebURL(for: state) else { 151 | return 152 | } 153 | if let progressBlock = progressBlock { 154 | progressBlock(receivedSize, totalSize) 155 | } 156 | }, 157 | completionHandler: { [weak base] image, error, cacheType, imageURL in 158 | DispatchQueue.main.safeAsync { 159 | guard let strongBase = base, imageURL == self.backgroundWebURL(for: state) else { 160 | return 161 | } 162 | self.setBackgroundImageTask(nil) 163 | if image != nil { 164 | strongBase.setBackgroundImage(image, for: state) 165 | } 166 | completionHandler?(image, error, cacheType, imageURL) 167 | } 168 | }) 169 | 170 | setBackgroundImageTask(task) 171 | return task 172 | } 173 | 174 | /** 175 | Cancel the background image download task bounded to the image view if it is running. 176 | Nothing will happen if the downloading has already finished. 177 | */ 178 | public func cancelBackgroundImageDownloadTask() { 179 | backgroundImageTask?.cancel() 180 | } 181 | 182 | } 183 | 184 | // MARK: - Associated Object 185 | private var lastURLKey: Void? 186 | private var imageTaskKey: Void? 187 | 188 | extension Kingfisher where Base: UIButton { 189 | /** 190 | Get the image URL binded to this button for a specified state. 191 | 192 | - parameter state: The state that uses the specified image. 193 | 194 | - returns: Current URL for image. 195 | */ 196 | public func webURL(for state: UIControlState) -> URL? { 197 | return webURLs[NSNumber(value:state.rawValue)] as? URL 198 | } 199 | 200 | fileprivate func setWebURL(_ url: URL?, for state: UIControlState) { 201 | webURLs[NSNumber(value:state.rawValue)] = url 202 | } 203 | 204 | fileprivate var webURLs: NSMutableDictionary { 205 | var dictionary = objc_getAssociatedObject(base, &lastURLKey) as? NSMutableDictionary 206 | if dictionary == nil { 207 | dictionary = NSMutableDictionary() 208 | setWebURLs(dictionary!) 209 | } 210 | return dictionary! 211 | } 212 | 213 | fileprivate func setWebURLs(_ URLs: NSMutableDictionary) { 214 | objc_setAssociatedObject(base, &lastURLKey, URLs, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 215 | } 216 | 217 | fileprivate var imageTask: RetrieveImageTask? { 218 | return objc_getAssociatedObject(base, &imageTaskKey) as? RetrieveImageTask 219 | } 220 | 221 | fileprivate func setImageTask(_ task: RetrieveImageTask?) { 222 | objc_setAssociatedObject(base, &imageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 223 | } 224 | } 225 | 226 | 227 | private var lastBackgroundURLKey: Void? 228 | private var backgroundImageTaskKey: Void? 229 | 230 | 231 | extension Kingfisher where Base: UIButton { 232 | /** 233 | Get the background image URL binded to this button for a specified state. 234 | 235 | - parameter state: The state that uses the specified background image. 236 | 237 | - returns: Current URL for background image. 238 | */ 239 | public func backgroundWebURL(for state: UIControlState) -> URL? { 240 | return backgroundWebURLs[NSNumber(value:state.rawValue)] as? URL 241 | } 242 | 243 | fileprivate func setBackgroundWebURL(_ url: URL?, for state: UIControlState) { 244 | backgroundWebURLs[NSNumber(value:state.rawValue)] = url 245 | } 246 | 247 | fileprivate var backgroundWebURLs: NSMutableDictionary { 248 | var dictionary = objc_getAssociatedObject(base, &lastBackgroundURLKey) as? NSMutableDictionary 249 | if dictionary == nil { 250 | dictionary = NSMutableDictionary() 251 | setBackgroundWebURLs(dictionary!) 252 | } 253 | return dictionary! 254 | } 255 | 256 | fileprivate func setBackgroundWebURLs(_ URLs: NSMutableDictionary) { 257 | objc_setAssociatedObject(base, &lastBackgroundURLKey, URLs, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 258 | } 259 | 260 | fileprivate var backgroundImageTask: RetrieveImageTask? { 261 | return objc_getAssociatedObject(base, &backgroundImageTaskKey) as? RetrieveImageTask 262 | } 263 | 264 | fileprivate func setBackgroundImageTask(_ task: RetrieveImageTask?) { 265 | objc_setAssociatedObject(base, &backgroundImageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 266 | } 267 | } 268 | 269 | // MARK: - Deprecated. Only for back compatibility. 270 | /** 271 | * Set image to use from web for a specified state. Deprecated. Use `kf` namespacing instead. 272 | */ 273 | extension UIButton { 274 | /** 275 | Set an image to use for a specified state with a resource, a placeholder image, options, progress handler and 276 | completion handler. 277 | 278 | - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. 279 | - parameter state: The state that uses the specified image. 280 | - parameter placeholder: A placeholder image when retrieving the image at URL. 281 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 282 | - parameter progressBlock: Called when the image downloading progress gets updated. 283 | - parameter completionHandler: Called when the image retrieved and set. 284 | 285 | - returns: A task represents the retrieving process. 286 | 287 | - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. 288 | The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. 289 | */ 290 | @discardableResult 291 | @available(*, deprecated, 292 | message: "Extensions directly on UIButton are deprecated. Use `button.kf.setImage` instead.", 293 | renamed: "kf.setImage") 294 | public func kf_setImage(with resource: Resource?, 295 | for state: UIControlState, 296 | placeholder: UIImage? = nil, 297 | options: KingfisherOptionsInfo? = nil, 298 | progressBlock: DownloadProgressBlock? = nil, 299 | completionHandler: CompletionHandler? = nil) -> RetrieveImageTask 300 | { 301 | return kf.setImage(with: resource, for: state, placeholder: placeholder, options: options, 302 | progressBlock: progressBlock, completionHandler: completionHandler) 303 | } 304 | 305 | /** 306 | Cancel the image download task bounded to the image view if it is running. 307 | Nothing will happen if the downloading has already finished. 308 | */ 309 | @available(*, deprecated, 310 | message: "Extensions directly on UIButton are deprecated. Use `button.kf.cancelImageDownloadTask` instead.", 311 | renamed: "kf.cancelImageDownloadTask") 312 | public func kf_cancelImageDownloadTask() { kf.cancelImageDownloadTask() } 313 | 314 | /** 315 | Set the background image to use for a specified state with a resource, 316 | a placeholder image, options progress handler and completion handler. 317 | 318 | - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. 319 | - parameter state: The state that uses the specified image. 320 | - parameter placeholder: A placeholder image when retrieving the image at URL. 321 | - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. 322 | - parameter progressBlock: Called when the image downloading progress gets updated. 323 | - parameter completionHandler: Called when the image retrieved and set. 324 | 325 | - returns: A task represents the retrieving process. 326 | 327 | - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. 328 | The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. 329 | */ 330 | @discardableResult 331 | @available(*, deprecated, 332 | message: "Extensions directly on UIButton are deprecated. Use `button.kf.setBackgroundImage` instead.", 333 | renamed: "kf.setBackgroundImage") 334 | public func kf_setBackgroundImage(with resource: Resource?, 335 | for state: UIControlState, 336 | placeholder: UIImage? = nil, 337 | options: KingfisherOptionsInfo? = nil, 338 | progressBlock: DownloadProgressBlock? = nil, 339 | completionHandler: CompletionHandler? = nil) -> RetrieveImageTask 340 | { 341 | return kf.setBackgroundImage(with: resource, for: state, placeholder: placeholder, options: options, 342 | progressBlock: progressBlock, completionHandler: completionHandler) 343 | } 344 | 345 | /** 346 | Cancel the background image download task bounded to the image view if it is running. 347 | Nothing will happen if the downloading has already finished. 348 | */ 349 | @available(*, deprecated, 350 | message: "Extensions directly on UIButton are deprecated. Use `button.kf.cancelBackgroundImageDownloadTask` instead.", 351 | renamed: "kf.cancelBackgroundImageDownloadTask") 352 | public func kf_cancelBackgroundImageDownloadTask() { kf.cancelBackgroundImageDownloadTask() } 353 | 354 | /** 355 | Get the image URL binded to this button for a specified state. 356 | 357 | - parameter state: The state that uses the specified image. 358 | 359 | - returns: Current URL for image. 360 | */ 361 | @available(*, deprecated, 362 | message: "Extensions directly on UIButton are deprecated. Use `button.kf.webURL` instead.", 363 | renamed: "kf.webURL") 364 | public func kf_webURL(for state: UIControlState) -> URL? { return kf.webURL(for: state) } 365 | 366 | @available(*, deprecated, message: "Extensions directly on UIButton are deprecated.",renamed: "kf.setWebURL") 367 | fileprivate func kf_setWebURL(_ url: URL, for state: UIControlState) { kf.setWebURL(url, for: state) } 368 | 369 | @available(*, deprecated, message: "Extensions directly on UIButton are deprecated.",renamed: "kf.webURLs") 370 | fileprivate var kf_webURLs: NSMutableDictionary { return kf.webURLs } 371 | 372 | @available(*, deprecated, message: "Extensions directly on UIButton are deprecated.",renamed: "kf.setWebURLs") 373 | fileprivate func kf_setWebURLs(_ URLs: NSMutableDictionary) { kf.setWebURLs(URLs) } 374 | 375 | @available(*, deprecated, message: "Extensions directly on UIButton are deprecated.",renamed: "kf.imageTask") 376 | fileprivate var kf_imageTask: RetrieveImageTask? { return kf.imageTask } 377 | 378 | @available(*, deprecated, message: "Extensions directly on UIButton are deprecated.",renamed: "kf.setImageTask") 379 | fileprivate func kf_setImageTask(_ task: RetrieveImageTask?) { kf.setImageTask(task) } 380 | 381 | /** 382 | Get the background image URL binded to this button for a specified state. 383 | 384 | - parameter state: The state that uses the specified background image. 385 | 386 | - returns: Current URL for background image. 387 | */ 388 | @available(*, deprecated, 389 | message: "Extensions directly on UIButton are deprecated. Use `button.kf.backgroundWebURL` instead.", 390 | renamed: "kf.backgroundWebURL") 391 | public func kf_backgroundWebURL(for state: UIControlState) -> URL? { return kf.backgroundWebURL(for: state) } 392 | 393 | @available(*, deprecated, 394 | message: "Extensions directly on UIButton are deprecated.",renamed: "kf.setBackgroundWebURL") 395 | fileprivate func kf_setBackgroundWebURL(_ url: URL, for state: UIControlState) { 396 | kf.setBackgroundWebURL(url, for: state) 397 | } 398 | 399 | @available(*, deprecated, 400 | message: "Extensions directly on UIButton are deprecated.",renamed: "kf.backgroundWebURLs") 401 | fileprivate var kf_backgroundWebURLs: NSMutableDictionary { return kf.backgroundWebURLs } 402 | 403 | @available(*, deprecated, 404 | message: "Extensions directly on UIButton are deprecated.",renamed: "kf.setBackgroundWebURLs") 405 | fileprivate func kf_setBackgroundWebURLs(_ URLs: NSMutableDictionary) { kf.setBackgroundWebURLs(URLs) } 406 | 407 | @available(*, deprecated, 408 | message: "Extensions directly on UIButton are deprecated.",renamed: "kf.backgroundImageTask") 409 | fileprivate var kf_backgroundImageTask: RetrieveImageTask? { return kf.backgroundImageTask } 410 | 411 | @available(*, deprecated, 412 | message: "Extensions directly on UIButton are deprecated.",renamed: "kf.setBackgroundImageTask") 413 | fileprivate func kf_setBackgroundImageTask(_ task: RetrieveImageTask?) { return kf.setBackgroundImageTask(task) } 414 | 415 | } 416 | -------------------------------------------------------------------------------- /Linkage/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Linkage/TableView/CategoryModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryModel.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/3. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CategoryModel: NSObject { 12 | 13 | var name : String? 14 | var icon : String? 15 | var spus : [FoodModel]? 16 | 17 | init(dict : [String : Any]) { 18 | super.init() 19 | setValuesForKeys(dict) 20 | } 21 | 22 | override func setValue(_ value: Any?, forKey key: String) { 23 | if key == "spus" { 24 | spus = Array() 25 | guard let datas = value as? [[String : Any]] else { return } 26 | for dict in datas { 27 | let foodModel = FoodModel(dict: dict) 28 | spus?.append(foodModel) 29 | } 30 | } else { 31 | super.setValue(value, forKey: key) 32 | } 33 | } 34 | 35 | override func setValue(_ value: Any?, forUndefinedKey key: String) { 36 | 37 | } 38 | 39 | } 40 | 41 | class FoodModel: NSObject { 42 | 43 | var name : String? 44 | var picture : String? 45 | var minPrice : Float? 46 | 47 | init(dict : [String : Any]) { 48 | super.init() 49 | setValuesForKeys(dict) 50 | } 51 | 52 | override func setValue(_ value: Any?, forKey key: String) { 53 | if key == "min_price" { 54 | guard let price = value as? Float else {return} 55 | minPrice = price 56 | } else { 57 | super.setValue(value, forKey: key) 58 | } 59 | } 60 | 61 | override func setValue(_ value: Any?, forUndefinedKey key: String) { 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Linkage/TableView/LeftTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeftTableViewCell.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/3. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LeftTableViewCell: UITableViewCell { 12 | 13 | lazy var nameLabel = UILabel() 14 | private lazy var yellowView = UIView() 15 | 16 | override func awakeFromNib() { 17 | super.awakeFromNib() 18 | // Initialization code 19 | } 20 | 21 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 22 | super.init(style: style, reuseIdentifier: reuseIdentifier) 23 | 24 | selectionStyle = .none 25 | 26 | configureUI() 27 | } 28 | 29 | func configureUI () { 30 | 31 | nameLabel.frame = CGRect(x: 10, y: 10, width: 60, height: 40) 32 | nameLabel.numberOfLines = 0 33 | nameLabel.font = UIFont.systemFont(ofSize: 15) 34 | nameLabel.textColor = UIColor(130, 130, 130) 35 | nameLabel.highlightedTextColor = UIColor(253, 212, 49) 36 | contentView.addSubview(nameLabel) 37 | 38 | yellowView.frame = CGRect(x: 0, y: 5, width: 5, height: 45) 39 | yellowView.backgroundColor = UIColor(253, 212, 49) 40 | contentView.addSubview(yellowView) 41 | 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | override func setSelected(_ selected: Bool, animated: Bool) { 49 | super.setSelected(selected, animated: animated) 50 | 51 | contentView.backgroundColor = selected ? UIColor.white : UIColor(white: 0, alpha: 0.1) 52 | isHighlighted = selected 53 | nameLabel.isHighlighted = selected 54 | yellowView.isHidden = !selected 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Linkage/TableView/RightTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RightTableViewCell.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/3. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RightTableViewCell: UITableViewCell { 12 | 13 | private lazy var nameLabel = UILabel() 14 | private lazy var imageV = UIImageView() 15 | private lazy var priceLabel = UILabel() 16 | 17 | override func awakeFromNib() { 18 | super.awakeFromNib() 19 | // Initialization code 20 | } 21 | 22 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 23 | super.init(style: style, reuseIdentifier: reuseIdentifier) 24 | configureUI() 25 | } 26 | 27 | func setDatas(_ model : FoodModel) { 28 | 29 | guard 30 | let minPrice = model.minPrice, 31 | let picture = model.picture, 32 | let name = model.name else { return } 33 | priceLabel.text = "¥\(minPrice)" 34 | nameLabel.text = name 35 | 36 | guard let url = URL.init(string: picture) else { return } 37 | 38 | imageV.kf.setImage(with: url) 39 | 40 | } 41 | 42 | func configureUI() { 43 | imageV.frame = CGRect(x: 15, y: 15, width: 50, height: 50) 44 | contentView.addSubview(imageV) 45 | 46 | nameLabel.frame = CGRect(x: 80, y: 10, width: 200, height: 30) 47 | nameLabel.font = UIFont.systemFont(ofSize: 14) 48 | contentView.addSubview(nameLabel) 49 | 50 | priceLabel.frame = CGRect(x: 80, y: 45, width: 200, height: 30) 51 | priceLabel.font = UIFont.systemFont(ofSize: 14) 52 | priceLabel.textColor = UIColor.red 53 | contentView.addSubview(priceLabel) 54 | } 55 | 56 | required init?(coder aDecoder: NSCoder) { 57 | fatalError("init(coder:) has not been implemented") 58 | } 59 | 60 | override func setSelected(_ selected: Bool, animated: Bool) { 61 | super.setSelected(selected, animated: animated) 62 | 63 | // Configure the view for the selected state 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Linkage/TableView/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/3. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 12 | 13 | fileprivate lazy var leftTableView : UITableView = { 14 | let leftTableView = UITableView() 15 | leftTableView.delegate = self 16 | leftTableView.dataSource = self 17 | leftTableView.frame = CGRect(x: 0, y: 0, width: 80, height: ScreenHeight) 18 | leftTableView.rowHeight = 55 19 | leftTableView.showsVerticalScrollIndicator = false 20 | leftTableView.separatorColor = UIColor.clear 21 | leftTableView.register(LeftTableViewCell.self, forCellReuseIdentifier: kLeftTableViewCell) 22 | return leftTableView 23 | }() 24 | 25 | fileprivate lazy var rightTableView : UITableView = { 26 | let rightTableView = UITableView() 27 | rightTableView.delegate = self 28 | rightTableView.dataSource = self 29 | rightTableView.frame = CGRect(x: 80, y: 64, width: ScreenWidth - 80, height: ScreenHeight - 64) 30 | rightTableView.rowHeight = 80 31 | rightTableView.showsVerticalScrollIndicator = false 32 | rightTableView.register(RightTableViewCell.self, forCellReuseIdentifier: kRightTableViewCell) 33 | return rightTableView 34 | }() 35 | 36 | fileprivate lazy var categoryData = [CategoryModel]() 37 | fileprivate lazy var foodData = [[FoodModel]]() 38 | 39 | fileprivate var selectIndex = 0 40 | fileprivate var isScrollDown = true 41 | fileprivate var lastOffsetY : CGFloat = 0.0 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | view.backgroundColor = UIColor.white 47 | 48 | configureData() 49 | 50 | view.addSubview(leftTableView) 51 | view.addSubview(rightTableView) 52 | 53 | leftTableView.selectRow(at: IndexPath(row: 0, section: 0), animated: true, scrollPosition: .none) 54 | } 55 | } 56 | 57 | //MARK: - 获取数据 58 | extension TableViewController { 59 | 60 | fileprivate func configureData () { 61 | 62 | guard let path = Bundle.main.path(forResource: "meituan", ofType: "json") else { return } 63 | 64 | guard let data = NSData(contentsOfFile: path) as Data? else { return } 65 | 66 | guard let anyObject = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else { return } 67 | 68 | guard let dict = anyObject as? [String : Any] else { return } 69 | 70 | guard let datas = dict["data"] as? [String : Any] else { return } 71 | 72 | guard let foods = datas["food_spu_tags"] as? [[String : Any]] else { return } 73 | 74 | for food in foods { 75 | 76 | let model = CategoryModel(dict: food) 77 | categoryData.append(model) 78 | 79 | guard let spus = model.spus else { continue } 80 | var datas = [FoodModel]() 81 | for fModel in spus { 82 | datas.append(fModel) 83 | } 84 | foodData.append(datas) 85 | } 86 | } 87 | } 88 | 89 | //MARK: - TableView DataSource Delegate 90 | extension TableViewController { 91 | 92 | func numberOfSections(in tableView: UITableView) -> Int { 93 | if leftTableView == tableView { 94 | return 1 95 | } else { 96 | return categoryData.count 97 | } 98 | } 99 | 100 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 101 | if leftTableView == tableView { 102 | return categoryData.count 103 | } else { 104 | return foodData[section].count 105 | } 106 | } 107 | 108 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 109 | if leftTableView == tableView { 110 | let cell = tableView.dequeueReusableCell(withIdentifier: kLeftTableViewCell, for: indexPath) as! LeftTableViewCell 111 | let model = categoryData[indexPath.row] 112 | cell.nameLabel.text = model.name 113 | return cell 114 | } else { 115 | let cell = tableView.dequeueReusableCell(withIdentifier: kRightTableViewCell, for: indexPath) as! RightTableViewCell 116 | let model = foodData[indexPath.section][indexPath.row] 117 | cell.setDatas(model) 118 | return cell 119 | } 120 | 121 | } 122 | 123 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 124 | if leftTableView == tableView { 125 | return 0 126 | } 127 | return 20 128 | } 129 | 130 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 131 | if leftTableView == tableView { 132 | return nil 133 | } 134 | let headerView = TableViewHeaderView(frame: CGRect(x: 0, y: 0, width: ScreenWidth, height: 20)) 135 | let model = categoryData[section] 136 | headerView.nameLabel.text = model.name 137 | return headerView 138 | } 139 | 140 | // TableView 分区标题即将展示 141 | func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { 142 | // 当前的 tableView 是 RightTableView,RightTableView 滚动的方向向上,RightTableView 是用户拖拽而产生滚动的((主要判断 RightTableView 用户拖拽而滚动的,还是点击 LeftTableView 而滚动的) 143 | if (rightTableView == tableView) 144 | && !isScrollDown 145 | && (rightTableView.isDragging || rightTableView.isDecelerating) { 146 | selectRow(index: section) 147 | } 148 | } 149 | 150 | // TableView分区标题展示结束 151 | func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) { 152 | // 当前的 tableView 是 RightTableView,RightTableView 滚动的方向向下,RightTableView 是用户拖拽而产生滚动的((主要判断 RightTableView 用户拖拽而滚动的,还是点击 LeftTableView 而滚动的) 153 | if (rightTableView == tableView) 154 | && isScrollDown 155 | && (rightTableView.isDragging || rightTableView.isDecelerating) { 156 | selectRow(index: section + 1) 157 | } 158 | } 159 | 160 | // 当拖动右边 TableView 的时候,处理左边 TableView 161 | private func selectRow(index : Int) { 162 | leftTableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .top) 163 | } 164 | 165 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 166 | if leftTableView == tableView { 167 | selectIndex = indexPath.row 168 | rightTableView.scrollToRow(at: IndexPath(row: 0, section: selectIndex), at: .top, animated: true) 169 | leftTableView.scrollToRow(at: IndexPath(row: selectIndex, section: 0), at: .top, animated: true) 170 | } 171 | } 172 | 173 | // 标记一下 RightTableView 的滚动方向,是向上还是向下 174 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 175 | 176 | let tableView = scrollView as! UITableView 177 | if rightTableView == tableView { 178 | isScrollDown = lastOffsetY < scrollView.contentOffset.y 179 | lastOffsetY = scrollView.contentOffset.y 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Linkage/TableView/TableViewHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewHeaderView.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/10. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TableViewHeaderView: UIView { 12 | 13 | lazy var nameLabel = UILabel() 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | 18 | backgroundColor = UIColor(240, 240, 240, 0.8) 19 | nameLabel.frame = CGRect(x: 15, y: 0, width: 200, height: 20) 20 | nameLabel.font = UIFont.systemFont(ofSize: 13) 21 | addSubview(nameLabel) 22 | 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Linkage/Tools/LJCollectionViewFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LJCollectionViewFlowLayout.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/10. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LJCollectionViewFlowLayout: UICollectionViewFlowLayout { 12 | 13 | // 导航栏高度,默认为0 14 | var naviHeight: CGFloat = 0.0 15 | 16 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 17 | // UICollectionViewLayoutAttributes:我称它为collectionView中的item(包括cell和header、footer这些)的《结构信息》 18 | var attributesArray: [UICollectionViewLayoutAttributes] = [] 19 | if let superAttributesArray = super.layoutAttributesForElements(in: rect) { 20 | attributesArray = superAttributesArray 21 | } 22 | 23 | // 创建存索引的数组,无符号(正整数),无序(不能通过下标取值),不可重复(重复的话会自动过滤) 24 | let noneHeaderSections = NSMutableIndexSet() 25 | // 遍历superArray,得到一个当前屏幕中所有的section数组 26 | for attributes in attributesArray { 27 | // 如果当前的元素分类是一个cell,将cell所在的分区section加入数组,重复的话会自动过滤 28 | if attributes.representedElementCategory == .cell { 29 | noneHeaderSections.add(attributes.indexPath.section) 30 | } 31 | } 32 | 33 | // 遍历superArray,将当前屏幕中拥有的header的section从数组中移除,得到一个当前屏幕中没有header的section数组 34 | // 正常情况下,随着手指往上移,header脱离屏幕会被系统回收而cell尚在,也会触发该方法 35 | for attributes in attributesArray { 36 | if let kind = attributes.representedElementKind { 37 | //如果当前的元素是一个header,将header所在的section从数组中移除 38 | if kind == UICollectionElementKindSectionHeader { 39 | noneHeaderSections.remove(attributes.indexPath.section) 40 | } 41 | } 42 | } 43 | 44 | // 遍历当前屏幕中没有header的section数组 45 | noneHeaderSections.enumerate({ (index, stop) in 46 | // 取到当前section中第一个item的indexPath 47 | let indexPath = IndexPath(row: 0, section: index) 48 | // 获取当前section在正常情况下已经离开屏幕的header结构信息 49 | if let attributes = self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: indexPath) { 50 | // 如果当前分区确实有因为离开屏幕而被系统回收的header,将该header结构信息重新加入到superArray中去 51 | attributesArray.append(attributes) 52 | } 53 | }) 54 | 55 | // 遍历superArray,改变header结构信息中的参数,使它可以在当前section还没完全离开屏幕的时候一直显示 56 | for attributes in attributesArray { 57 | if attributes.representedElementKind == UICollectionElementKindSectionHeader { 58 | let section = attributes.indexPath.section 59 | 60 | let firstItemIndexPath = IndexPath(row: 0, section: section) 61 | 62 | var numberOfItemsInSection = 0 63 | // 得到当前header所在分区的cell的数量 64 | if let number = self.collectionView?.numberOfItems(inSection: section) { 65 | numberOfItemsInSection = number 66 | } 67 | 68 | // 得到最后一个item的indexPath 69 | let lastItemIndexPath = IndexPath(row: max(0, numberOfItemsInSection-1), section: section) 70 | 71 | // 得到第一个item和最后一个item的结构信息 72 | let firstItemAttributes: UICollectionViewLayoutAttributes! 73 | let lastItemAttributes: UICollectionViewLayoutAttributes! 74 | if numberOfItemsInSection > 0 { 75 | // cell有值,则获取第一个cell和最后一个cell的结构信息 76 | firstItemAttributes = self.layoutAttributesForItem(at: firstItemIndexPath) 77 | lastItemAttributes = self.layoutAttributesForItem(at: lastItemIndexPath) 78 | } else { 79 | // cell没值,就新建一个UICollectionViewLayoutAttributes 80 | firstItemAttributes = UICollectionViewLayoutAttributes() 81 | // 然后模拟出在当前分区中的唯一一个cell,cell在header的下面,高度为0,还与header隔着可能存在的sectionInset的top 82 | let itemY = attributes.frame.maxY + self.sectionInset.top 83 | firstItemAttributes.frame = CGRect(x: 0, y: itemY, width: 0, height: 0) 84 | // 因为只有一个cell,所以最后一个cell等于第一个cell 85 | lastItemAttributes = firstItemAttributes 86 | } 87 | 88 | // 获取当前header的frame 89 | var rect = attributes.frame 90 | 91 | // 当前的滑动距离 + 因为导航栏产生的偏移量,默认为64(如果app需求不同,需自己设置) 92 | var offset_Y: CGFloat = 0 93 | if let y = self.collectionView?.contentOffset.y { 94 | offset_Y = y 95 | } 96 | offset_Y = offset_Y + naviHeight 97 | 98 | // 第一个cell的y值 - 当前header的高度 - 可能存在的sectionInset的top 99 | let headerY = firstItemAttributes.frame.origin.y - rect.size.height - self.sectionInset.top 100 | 101 | // 哪个大取哪个,保证header悬停 102 | // 针对当前header基本上都是offset更加大,针对下一个header则会是headerY大,各自处理 103 | let maxY = max(offset_Y, headerY) 104 | 105 | // 最后一个cell的y值 + 最后一个cell的高度 + 可能存在的sectionInset的bottom - 当前header的高度 106 | // 当当前section的footer或者下一个section的header接触到当前header的底部,计算出的headerMissingY即为有效值 107 | let headerMissingY = lastItemAttributes.frame.maxY + self.sectionInset.bottom - rect.size.height 108 | 109 | // 给rect的y赋新值,因为在最后消失的临界点要跟谁消失,所以取小 110 | rect.origin.y = min(maxY, headerMissingY) 111 | // 给header的结构信息的frame重新赋值 112 | attributes.frame = rect 113 | 114 | // 如果按照正常情况下,header离开屏幕被系统回收,而header的层次关系又与cell相等,如果不去理会,会出现cell在header上面的情况 115 | // 通过打印可以知道cell的层次关系zIndex数值为0,我们可以将header的zIndex设置成1,如果不放心,也可以将它设置成非常大 116 | attributes.zIndex = 1024 117 | } 118 | } 119 | 120 | return attributesArray 121 | } 122 | 123 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 124 | return true 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /Linkage/Tools/UIColor-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor-Extension.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/3/3. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | convenience init(_ r : CGFloat, _ g : CGFloat, _ b : CGFloat) { 14 | 15 | let red = r / 255.0 16 | let green = g / 255.0 17 | let blue = b / 255.0 18 | 19 | self.init(red: red, green: green, blue: blue, alpha: 1) 20 | } 21 | 22 | convenience init(_ r : CGFloat, _ g : CGFloat, _ b : CGFloat, _ a : CGFloat) { 23 | 24 | let red = r / 255.0 25 | let green = g / 255.0 26 | let blue = b / 255.0 27 | 28 | self.init(red: red, green: green, blue: blue, alpha: a) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Linkage/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Linkage 4 | // 5 | // Created by LeeJay on 2017/2/28. 6 | // Copyright © 2017年 LeeJay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UITableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | let datas = ["UITableView", "UICollectionView"] 19 | 20 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 21 | return datas.count 22 | } 23 | 24 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 25 | let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath) 26 | cell.textLabel?.text = datas[indexPath.row] 27 | return cell 28 | } 29 | 30 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 31 | tableView.deselectRow(at: indexPath, animated: true) 32 | 33 | if indexPath.row == 0 { 34 | let table = TableViewController() 35 | table.title = datas[indexPath.row] 36 | navigationController?.pushViewController(table, animated: true) 37 | } else { 38 | let collection = CollectionViewController() 39 | collection.title = datas[indexPath.row] 40 | navigationController?.pushViewController(collection, animated: true) 41 | } 42 | } 43 | 44 | override func didReceiveMemoryWarning() { 45 | super.didReceiveMemoryWarning() 46 | // Dispose of any resources that can be recreated. 47 | } 48 | 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 【Swift 联动】:两个 TableView 之间的联动,TableView 与 CollectionView 之间的联动 2 | 3 | ## 前言 4 | 之前用 Objective-C 写了一篇联动的 demo 和文章,后来有小伙伴私信我有没有 Swfit 语言的,最近趁晚上和周末学习了一下 Swift 3.0 的语法,写了一个 Swift 的 demo。 5 | 思路和 [Objective-C 版本的联动文章](http://www.jianshu.com/p/7e534656988d)一样,实现的效果也是一样。先来看下效果图。 6 | 7 | ![联动.gif](http://upload-images.jianshu.io/upload_images/1321491-c779e8ced78e89c0.gif?imageMogr2/auto-orient/strip) 8 | 9 | ## 正文 10 | 11 | ### 一、TableView 与 TableView 之间的联动 12 | 下面来说下实现两个 TableView 之间联动的主要思路: 13 | 先解析数据装入模型。 14 | 15 | ```swift 16 | // 数据校验 17 | guard let path = Bundle.main.path(forResource: "meituan", ofType: "json") else { return } 18 | 19 | guard let data = NSData(contentsOfFile: path) as? Data else { return } 20 | 21 | guard let anyObject = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else { return } 22 | 23 | guard let dict = anyObject as? [String : Any] else { return } 24 | 25 | guard let datas = dict["data"] as? [String : Any] else { return } 26 | 27 | guard let foods = datas["food_spu_tags"] as? [[String : Any]] else { return } 28 | 29 | for food in foods { 30 | 31 | let model = CategoryModel(dict: food) 32 | categoryData.append(model) 33 | 34 | guard let spus = model.spus else { continue } 35 | var datas = [FoodModel]() 36 | for fModel in spus { 37 | datas.append(fModel) 38 | } 39 | foodData.append(datas) 40 | } 41 | ``` 42 | 43 | 定义两个 TableView:LeftTableView 和 RightTableView。 44 | 45 | ```swift 46 | fileprivate lazy var leftTableView : UITableView = { 47 | let leftTableView = UITableView() 48 | leftTableView.delegate = self 49 | leftTableView.dataSource = self 50 | leftTableView.frame = CGRect(x: 0, y: 0, width: 80, height: ScreenHeight) 51 | leftTableView.rowHeight = 55 52 | leftTableView.showsVerticalScrollIndicator = false 53 | leftTableView.separatorColor = UIColor.clear 54 | leftTableView.register(LeftTableViewCell.self, forCellReuseIdentifier: kLeftTableViewCell) 55 | return leftTableView 56 | }() 57 | 58 | fileprivate lazy var rightTableView : UITableView = { 59 | let rightTableView = UITableView() 60 | rightTableView.delegate = self 61 | rightTableView.dataSource = self 62 | rightTableView.frame = CGRect(x: 80, y: 64, width: ScreenWidth - 80, height: ScreenHeight - 64) 63 | rightTableView.rowHeight = 80 64 | rightTableView.showsVerticalScrollIndicator = false 65 | rightTableView.register(RightTableViewCell.self, forCellReuseIdentifier: kRightTableViewCell) 66 | return rightTableView 67 | }() 68 | 69 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 70 | if leftTableView == tableView { 71 | let cell = tableView.dequeueReusableCell(withIdentifier: kLeftTableViewCell, for: indexPath) as! LeftTableViewCell 72 | let model = categoryData[indexPath.row] 73 | cell.nameLabel.text = model.name 74 | return cell 75 | } else { 76 | let cell = tableView.dequeueReusableCell(withIdentifier: kRightTableViewCell, for: indexPath) as! RightTableViewCell 77 | let model = foodData[indexPath.section][indexPath.row] 78 | cell.setDatas(model) 79 | return cell 80 | } 81 | } 82 | ``` 83 | 先将左边的 TableView 关联右边的 TableView:点击左边的 TableViewCell,右边的 TableView 跳到相应的分区列表头部。 84 | 85 | ```swift 86 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 87 | if leftTableView == tableView { 88 | return nil 89 | } 90 | let headerView = TableViewHeaderView(frame: CGRect(x: 0, y: 0, width: ScreenWidth, height: 20)) 91 | let model = categoryData[section] 92 | headerView.nameLabel.text = model.name 93 | return headerView 94 | } 95 | ``` 96 | 再将右边的 TableView 关联左边的 TableView:标记一下RightTableView 的滚动方向,然后分别在 TableView 分区标题即将展示和展示结束的代理函数里面处理逻辑。 97 | 98 | * 1.在 TableView 分区标题即将展示里面,判断当前的 tableView 是 RightTableView,RightTableView 滑动的方向向上,RightTableView 是用户拖拽而产生滚动的(主要判断RightTableView 是用户拖拽的,还是点击 LeftTableView 滚动的),如果三者都成立,那么 LeftTableView 的选中行就是 RightTableView 的当前 section。 99 | * 2.在 TableView 分区标题展示结束里面,判断当前的 tableView 是 RightTableView,滑动的方向向下,RightTableView 是用户拖拽而产生滚动的,如果三者都成立,那么 LeftTableView 的选中行就是 RightTableView 的当前 section-1。 100 | 101 | ```swift 102 | // 标记一下 RightTableView 的滚动方向,是向上还是向下 103 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 104 | 105 | let tableView = scrollView as! UITableView 106 | if rightTableView == tableView { 107 | isScrollDown = lastOffsetY < scrollView.contentOffset.y 108 | lastOffsetY = scrollView.contentOffset.y 109 | } 110 | } 111 | 112 | // TableView分区标题即将展示 113 | func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { 114 | // 当前的tableView是RightTableView,RightTableView滚动的方向向上,RightTableView是用户拖拽而产生滚动的((主要判断RightTableView用户拖拽而滚动的,还是点击LeftTableView而滚动的) 115 | if (rightTableView == tableView) && !isScrollDown && rightTableView.isDragging { 116 | selectRow(index: section) 117 | } 118 | } 119 | 120 | // TableView分区标题展示结束 121 | func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) { 122 | // 当前的tableView是RightTableView,RightTableView滚动的方向向下,RightTableView是用户拖拽而产生滚动的((主要判断RightTableView用户拖拽而滚动的,还是点击LeftTableView而滚动的) 123 | if (rightTableView == tableView) && isScrollDown && rightTableView.isDragging { 124 | selectRow(index: section + 1) 125 | } 126 | } 127 | 128 | // 当拖动右边TableView的时候,处理左边TableView 129 | private func selectRow(index : Int) { 130 | leftTableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .top) 131 | } 132 | ``` 133 | 这样就实现了两个 TableView 之间的联动,是不是很简单。 134 | 135 | ### 二、TableView 与 CollectionView 之间的联动 136 | 137 | TableView 与 CollectionView 之间的联动与两个 TableView 之间的联动逻辑类似。 138 | 下面说下实现 TableView 与 CollectionView 之间的联动的主要思路: 139 | 还是一样,先解析数据装入模型。 140 | 141 | ```swift 142 | // 数据校验 143 | guard let path = Bundle.main.path(forResource: "liwushuo", ofType: "json") else { return } 144 | 145 | guard let data = NSData(contentsOfFile: path) as? Data else { return } 146 | 147 | guard let anyObject = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else { return } 148 | 149 | guard let dict = anyObject as? [String : Any] else { return } 150 | 151 | guard let datas = dict["data"] as? [String : Any] else { return } 152 | 153 | guard let categories = datas["categories"] as? [[String : Any]] else { return } 154 | 155 | for category in categories { 156 | let model = CollectionCategoryModel(dict: category) 157 | dataSource.append(model) 158 | 159 | guard let subcategories = model.subcategories else { continue } 160 | 161 | var datas = [SubCategoryModel]() 162 | for subcategory in subcategories { 163 | datas.append(subcategory) 164 | } 165 | collectionDatas.append(datas) 166 | } 167 | ``` 168 | 169 | 定义一个 TableView,一个 CollectionView。 170 | 171 | ```swift 172 | fileprivate lazy var tableView : UITableView = { 173 | let tableView = UITableView() 174 | tableView.delegate = self 175 | tableView.dataSource = self 176 | tableView.frame = CGRect(x: 0, y: 0, width: 80, height: ScreenHeight) 177 | tableView.rowHeight = 55 178 | tableView.showsVerticalScrollIndicator = false 179 | tableView.separatorColor = UIColor.clear 180 | tableView.register(LeftTableViewCell.self, forCellReuseIdentifier: kLeftTableViewCell) 181 | return tableView 182 | }() 183 | 184 | fileprivate lazy var flowlayout : LJCollectionViewFlowLayout = { 185 | let flowlayout = LJCollectionViewFlowLayout() 186 | flowlayout.scrollDirection = .vertical 187 | flowlayout.minimumLineSpacing = 2 188 | flowlayout.minimumInteritemSpacing = 2 189 | flowlayout.itemSize = CGSize(width: (ScreenWidth - 80 - 4 - 4) / 3, height: (ScreenWidth - 80 - 4 - 4) / 3 + 30) 190 | flowlayout.headerReferenceSize = CGSize(width: ScreenWidth, height: 30) 191 | return flowlayout 192 | }() 193 | 194 | fileprivate lazy var collectionView : UICollectionView = { 195 | let collectionView = UICollectionView(frame: CGRect.init(x: 2 + 80, y: 2 + 64, width: ScreenWidth - 80 - 4, height: ScreenHeight - 64 - 4), collectionViewLayout: self.flowlayout) 196 | collectionView.delegate = self 197 | collectionView.dataSource = self 198 | collectionView.showsVerticalScrollIndicator = false 199 | collectionView.showsHorizontalScrollIndicator = false 200 | collectionView.backgroundColor = UIColor.clear 201 | collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: kCollectionViewCell) 202 | collectionView.register(CollectionViewHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: kCollectionViewHeaderView) 203 | return collectionView 204 | }() 205 | 206 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 207 | let cell = tableView.dequeueReusableCell(withIdentifier: kLeftTableViewCell, for: indexPath) as! LeftTableViewCell 208 | let model = dataSource[indexPath.row] 209 | cell.nameLabel.text = model.name 210 | return cell 211 | } 212 | 213 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 214 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCollectionViewCell, for: indexPath) as! CollectionViewCell 215 | let model = collectionDatas[indexPath.section][indexPath.row] 216 | cell.setDatas(model) 217 | return cell 218 | } 219 | ``` 220 | 先将 TableView 关联 CollectionView,点击 TableViewCell,右边的 CollectionView 跳到相应的分区列表头部。 221 | 222 | ```swift 223 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 224 | selectIndex = indexPath.row 225 | collectionView.scrollToItem(at: IndexPath(row: 0, section: selectIndex), at: .top, animated: true) 226 | tableView.scrollToRow(at: IndexPath(row: selectIndex, section: 0), at: .top, animated: true) 227 | } 228 | ``` 229 | 再将 CollectionView 关联 TableView,标记一下 RightTableView 的滚动方向,然后分别在 CollectionView 分区标题即将展示和展示结束的代理函数里面处理逻辑。 230 | 231 | * 1.在 CollectionView 分区标题即将展示里面,判断 当前 CollectionView 滚动的方向向上, CollectionView 是用户拖拽而产生滚动的(主要是判断 CollectionView 是用户拖拽而滚动的,还是点击 TableView 而滚动的),如果二者都成立,那么 TableView 的选中行就是 CollectionView 的当前 section。 232 | * 2.在 CollectionView 分区标题展示结束里面,判断当前 CollectionView 滚动的方向向下, CollectionView 是用户拖拽而产生滚动的,如果二者都成立,那么 TableView 的选中行就是 CollectionView 的当前 section-1。 233 | 234 | ```swift 235 | // 标记一下 CollectionView 的滚动方向,是向上还是向下 236 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 237 | if collectionView == scrollView { 238 | isScrollDown = lastOffsetY < scrollView.contentOffset.y 239 | lastOffsetY = scrollView.contentOffset.y 240 | } 241 | } 242 | 243 | // CollectionView 分区标题即将展示 244 | func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) { 245 | // 当前 CollectionView 滚动的方向向上,CollectionView 是用户拖拽而产生滚动的(主要是判断 CollectionView 是用户拖拽而滚动的,还是点击 TableView 而滚动的) 246 | if !isScrollDown && collectionView.isDragging { 247 | selectRow(index: indexPath.section) 248 | } 249 | } 250 | 251 | // CollectionView 分区标题展示结束 252 | func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) { 253 | // 当前 CollectionView 滚动的方向向下,CollectionView 是用户拖拽而产生滚动的(主要是判断 CollectionView 是用户拖拽而滚动的,还是点击 TableView 而滚动的) 254 | if isScrollDown && collectionView.isDragging { 255 | selectRow(index: indexPath.section + 1) 256 | } 257 | } 258 | 259 | // 当拖动 CollectionView 的时候,处理 TableView 260 | private func selectRow(index : Int) { 261 | tableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .middle) 262 | } 263 | 264 | ``` 265 | TableView 与 CollectionView 之间的联动就这么实现了,是不是也很简单。 266 | 267 | ![TableView 与 CollectionView 之间的联动](http://upload-images.jianshu.io/upload_images/1321491-3e4c91db3ab471cf.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 268 | 269 | ## 最后 270 | 271 | 由于笔者水平有限,文中如果有错误的地方,或者有更好的方法,还望大神指出。 272 | 附上本文的所有 demo 下载链接,[【GitHub - Swift 版】](https://github.com/leejayID/Linkage-Swift)、[【GitHub - OC 版】](https://github.com/leejayID/Linkage),配合 demo 一起看文章,效果会更佳。 273 | 如果你看完后觉得对你有所帮助,还望在 GitHub 上点个 star。赠人玫瑰,手有余香。 274 | 275 | --------------------------------------------------------------------------------