├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------