版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.01.10 星期四 |
前言
iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
源码
1. Swift
首先看下工程组织结构
看下sb中的内容
下面就是源码了
1. Flickr.swift
import UIKit
let apiKey = "a35c883530bbe53c6db409d2a493991e"
class Flickr {
enum Error: Swift.Error {
case unknownAPIResponse
case generic
}
func searchFlickr(for searchTerm: String, completion: @escaping (Result<FlickrSearchResults>) -> Void) {
guard let searchURL = flickrSearchURL(for: searchTerm) else {
completion(Result.error(Error.unknownAPIResponse))
return
}
let searchRequest = URLRequest(url: searchURL)
URLSession.shared.dataTask(with: searchRequest) { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
completion(Result.error(error))
}
return
}
guard
let _ = response as? HTTPURLResponse,
let data = data
else {
DispatchQueue.main.async {
completion(Result.error(Error.unknownAPIResponse))
}
return
}
do {
guard
let resultsDictionary = try JSONSerialization.jsonObject(with: data) as? [String: AnyObject],
let stat = resultsDictionary["stat"] as? String
else {
DispatchQueue.main.async {
completion(Result.error(Error.unknownAPIResponse))
}
return
}
switch (stat) {
case "ok":
print("Results processed OK")
case "fail":
DispatchQueue.main.async {
completion(Result.error(Error.generic))
}
return
default:
DispatchQueue.main.async {
completion(Result.error(Error.unknownAPIResponse))
}
return
}
guard
let photosContainer = resultsDictionary["photos"] as? [String: AnyObject],
let photosReceived = photosContainer["photo"] as? [[String: AnyObject]]
else {
DispatchQueue.main.async {
completion(Result.error(Error.unknownAPIResponse))
}
return
}
let flickrPhotos: [FlickrPhoto] = photosReceived.compactMap { photoObject in
guard
let photoID = photoObject["id"] as? String,
let farm = photoObject["farm"] as? Int ,
let server = photoObject["server"] as? String ,
let secret = photoObject["secret"] as? String
else {
return nil
}
let flickrPhoto = FlickrPhoto(photoID: photoID, farm: farm, server: server, secret: secret)
guard
let url = flickrPhoto.flickrImageURL(),
let imageData = try? Data(contentsOf: url as URL)
else {
return nil
}
if let image = UIImage(data: imageData) {
flickrPhoto.thumbnail = image
return flickrPhoto
} else {
return nil
}
}
let searchResults = FlickrSearchResults(searchTerm: searchTerm, searchResults: flickrPhotos)
DispatchQueue.main.async {
completion(Result.results(searchResults))
}
} catch {
completion(Result.error(error))
return
}
}.resume()
}
private func flickrSearchURL(for searchTerm:String) -> URL? {
guard let escapedTerm = searchTerm.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics) else {
return nil
}
let URLString = "https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=\(apiKey)&text=\(escapedTerm)&per_page=20&format=json&nojsoncallback=1"
return URL(string:URLString)
}
}
2. FlickrPhoto.swift
import UIKit
class FlickrPhoto: Equatable {
var thumbnail: UIImage?
var largeImage: UIImage?
let photoID: String
let farm: Int
let server: String
let secret: String
init (photoID: String, farm: Int, server: String, secret: String) {
self.photoID = photoID
self.farm = farm
self.server = server
self.secret = secret
}
func flickrImageURL(_ size: String = "m") -> URL? {
if let url = URL(string: "https://farm\(farm).staticflickr.com/\(server)/\(photoID)_\(secret)_\(size).jpg") {
return url
}
return nil
}
enum Error: Swift.Error {
case invalidURL
case noData
}
func loadLargeImage(_ completion: @escaping (Result<FlickrPhoto>) -> Void) {
guard let loadURL = flickrImageURL("b") else {
DispatchQueue.main.async {
completion(Result.error(Error.invalidURL))
}
return
}
let loadRequest = URLRequest(url:loadURL)
URLSession.shared.dataTask(with: loadRequest) { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
completion(Result.error(error))
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(Result.error(Error.noData))
}
return
}
let returnedImage = UIImage(data: data)
self.largeImage = returnedImage
DispatchQueue.main.async {
completion(Result.results(self))
}
}.resume()
}
func sizeToFillWidth(of size:CGSize) -> CGSize {
guard let thumbnail = thumbnail else {
return size
}
let imageSize = thumbnail.size
var returnSize = size
let aspectRatio = imageSize.width / imageSize.height
returnSize.height = returnSize.width / aspectRatio
if returnSize.height > size.height {
returnSize.height = size.height
returnSize.width = size.height * aspectRatio
}
return returnSize
}
static func ==(lhs: FlickrPhoto, rhs: FlickrPhoto) -> Bool {
return lhs.photoID == rhs.photoID
}
}
3. FlickrSearchResults.swift
import Foundation
struct FlickrSearchResults {
let searchTerm: String
var searchResults: [FlickrPhoto]
}
4. Result.swift
import Foundation
enum Result<ResultType> {
case results(ResultType)
case error(Error)
}
5. AppDelegate.swift
import UIKit
let themeColor = UIColor(red: 0.01, green: 0.41, blue: 0.22, alpha: 1.0)
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
window?.tintColor = themeColor
return true
}
}
6. FlickrPhotosViewController.swift
import UIKit
final class FlickrPhotosViewController: UICollectionViewController {
// MARK: - Properties
private let reuseIdentifier = "FlickrCell"
private let sectionInsets = UIEdgeInsets(top: 50.0, left: 20.0, bottom: 50.0, right: 20.0)
private var searches: [FlickrSearchResults] = []
private let flickr = Flickr()
private let itemsPerRow: CGFloat = 3
private var selectedPhotos: [FlickrPhoto] = []
private let shareLabel = UILabel()
// 1
var largePhotoIndexPath: IndexPath? {
didSet {
// 2
var indexPaths: [IndexPath] = []
if let largePhotoIndexPath = largePhotoIndexPath {
indexPaths.append(largePhotoIndexPath)
}
if let oldValue = oldValue {
indexPaths.append(oldValue)
}
// 3
collectionView.performBatchUpdates({
self.collectionView.reloadItems(at: indexPaths)
}) { _ in
// 4
if let largePhotoIndexPath = self.largePhotoIndexPath {
self.collectionView.scrollToItem(at: largePhotoIndexPath,
at: .centeredVertically,
animated: true)
}
}
}
}
var sharing: Bool = false {
didSet {
// 1
collectionView.allowsMultipleSelection = sharing
// 2
collectionView.selectItem(at: nil, animated: true, scrollPosition: [])
selectedPhotos.removeAll()
guard let shareButton = self.navigationItem.rightBarButtonItems?.first else {
return
}
// 3
guard sharing else {
navigationItem.setRightBarButton(shareButton, animated: true)
return
}
// 4
if largePhotoIndexPath != nil {
largePhotoIndexPath = nil
}
// 5
updateSharedPhotoCountLabel()
// 6
let sharingItem = UIBarButtonItem(customView: shareLabel)
let items: [UIBarButtonItem] = [
shareButton,
sharingItem
]
navigationItem.setRightBarButtonItems(items, animated: true)
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dragInteractionEnabled = true
collectionView.dragDelegate = self
collectionView.dropDelegate = self
}
@IBAction func share(_ sender: UIBarButtonItem) {
guard !searches.isEmpty else {
return
}
guard !selectedPhotos.isEmpty else {
sharing.toggle()
return
}
guard sharing else {
return
}
let images: [UIImage] = selectedPhotos.compactMap { photo in
if let thumbnail = photo.thumbnail {
return thumbnail
}
return nil
}
guard !images.isEmpty else {
return
}
let shareController = UIActivityViewController(activityItems: images,
applicationActivities: nil)
shareController.completionWithItemsHandler = { _, _, _, _ in
self.sharing = false
self.selectedPhotos.removeAll()
self.updateSharedPhotoCountLabel()
}
shareController.popoverPresentationController?.barButtonItem = sender
shareController.popoverPresentationController?.permittedArrowDirections = .any
present(shareController,
animated: true,
completion: nil)
}
}
// MARK: - Private
private extension FlickrPhotosViewController {
func photo(for indexPath: IndexPath) -> FlickrPhoto {
return searches[indexPath.section].searchResults[indexPath.row]
}
func removePhoto(at indexPath: IndexPath) {
searches[indexPath.section].searchResults.remove(at: indexPath.row)
}
func insertPhoto(_ flickrPhoto: FlickrPhoto, at indexPath: IndexPath) {
searches[indexPath.section].searchResults.insert(flickrPhoto, at: indexPath.row)
}
func performLargeImageFetch(for indexPath: IndexPath, flickrPhoto: FlickrPhoto) {
// 1
guard let cell = self.collectionView.cellForItem(at: indexPath) as? FlickrPhotoCell else {
return
}
// 2
cell.activityIndicator.startAnimating()
// 3
flickrPhoto.loadLargeImage { [weak self] result in
// 4
guard let self = self else {
return
}
// 5
switch result {
// 6
case .results(let photo):
if indexPath == self.largePhotoIndexPath {
cell.imageView.image = photo.largeImage
}
case .error(_):
return
}
}
}
func updateSharedPhotoCountLabel() {
if sharing {
shareLabel.text = "\(selectedPhotos.count) photos selected"
} else {
shareLabel.text = ""
}
shareLabel.textColor = themeColor
UIView.animate(withDuration: 0.3) {
self.shareLabel.sizeToFit()
}
}
}
// MARK: - UITextFieldDelegate
extension FlickrPhotosViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
// 1
let activityIndicator = UIActivityIndicatorView(style: .gray)
textField.addSubview(activityIndicator)
activityIndicator.frame = textField.bounds
activityIndicator.startAnimating()
flickr.searchFlickr(for: textField.text!) { searchResults in
activityIndicator.removeFromSuperview()
switch searchResults {
case .error(let error):
print("Error Searching: \(error)")
case .results(let results):
print("Found \(results.searchResults.count) matching \(results.searchTerm)")
self.searches.insert(results, at: 0)
self.collectionView?.reloadData()
}
}
textField.text = nil
textField.resignFirstResponder()
return true
}
}
// MARK: - UICollectionViewDataSource
extension FlickrPhotosViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return searches.count
}
override func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return searches[section].searchResults.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: reuseIdentifier, for: indexPath) as? FlickrPhotoCell
else {
preconditionFailure("Invalid cell type")
}
let flickrPhoto = photo(for: indexPath)
// 1
cell.activityIndicator.stopAnimating()
// 2
guard indexPath == largePhotoIndexPath else {
cell.imageView.image = flickrPhoto.thumbnail
return cell
}
// 3
guard flickrPhoto.largeImage == nil else {
cell.imageView.image = flickrPhoto.largeImage
return cell
}
// 4
cell.imageView.image = flickrPhoto.thumbnail
// 5
performLargeImageFetch(for: indexPath, flickrPhoto: flickrPhoto)
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
// 1
switch kind {
// 2
case UICollectionView.elementKindSectionHeader:
// 3
guard let headerView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: "\(FlickrPhotoHeaderView.self)",
for: indexPath) as? FlickrPhotoHeaderView
else {
fatalError("Invalid view type")
}
let searchTerm = searches[indexPath.section].searchTerm
headerView.label.text = searchTerm
return headerView
default:
// 4
assert(false, "Invalid element type")
}
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension FlickrPhotosViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath == largePhotoIndexPath {
let flickrPhoto = photo(for: indexPath)
var size = collectionView.bounds.size
size.height -= (sectionInsets.top + sectionInsets.right)
size.width -= (sectionInsets.left + sectionInsets.right)
return flickrPhoto.sizeToFillWidth(of: size)
}
let paddingSpace = sectionInsets.left * (itemsPerRow + 1)
let availableWidth = view.frame.width - paddingSpace
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return sectionInsets
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return sectionInsets.left
}
}
// MARK: - UICollectionViewDelegate
extension FlickrPhotosViewController {
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
guard !sharing else {
return true
}
if largePhotoIndexPath == indexPath {
largePhotoIndexPath = nil
} else {
largePhotoIndexPath = indexPath
}
return false
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard sharing else {
return
}
let flickrPhoto = photo(for: indexPath)
selectedPhotos.append(flickrPhoto)
updateSharedPhotoCountLabel()
}
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
guard sharing else {
return
}
let flickrPhoto = photo(for: indexPath)
if let index = selectedPhotos.firstIndex(of: flickrPhoto) {
selectedPhotos.remove(at: index)
updateSharedPhotoCountLabel()
}
}
}
// MARK: - UICollectionViewDragDelegate
extension FlickrPhotosViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView,
itemsForBeginning session: UIDragSession,
at indexPath: IndexPath) -> [UIDragItem] {
let flickrPhoto = photo(for: indexPath)
guard let thumbnail = flickrPhoto.thumbnail else {
return []
}
let item = NSItemProvider(object: thumbnail)
let dragItem = UIDragItem(itemProvider: item)
return [dragItem]
}
}
// MARK: - UICollectionViewDropDelegate
extension FlickrPhotosViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
// 1
guard let destinationIndexPath = coordinator.destinationIndexPath else {
return
}
// 2
coordinator.items.forEach { dropItem in
guard let sourceIndexPath = dropItem.sourceIndexPath else {
return
}
// 3
collectionView.performBatchUpdates({
let image = photo(for: sourceIndexPath)
removePhoto(at: sourceIndexPath)
insertPhoto(image, at: destinationIndexPath)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}, completion: { _ in
// 4
coordinator.drop(dropItem.dragItem,
toItemAt: destinationIndexPath)
})
}
}
func collectionView(_ collectionView: UICollectionView,
dropSessionDidUpdate session: UIDropSession,
withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
return UICollectionViewDropProposal(operation: .move,
intent: .insertAtDestinationIndexPath)
}
}
7. FlickrPhotoCell.swift
import UIKit
class FlickrPhotoCell: UICollectionViewCell {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override var isSelected: Bool {
didSet {
imageView.layer.borderWidth = isSelected ? 10 : 0
}
}
override func awakeFromNib() {
super.awakeFromNib()
imageView.layer.borderColor = themeColor.cgColor
isSelected = false
}
}
8. FlickrPhotoHeaderView.swift
import UIKit
class FlickrPhotoHeaderView: UICollectionReusableView {
@IBOutlet weak var label: UILabel!
}
后记
本篇主要讲述了UICollectionView的重用、选择和重排序,感兴趣的给个赞或者关注~~~