v
源码
1. Swift
首先看下工程组织结构
看下sb中的内容
下面就是源码了
1. Flickr.swift
import UIKitlet 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 UIKitclass 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 Foundationenum Result<ResultType> { case results(ResultType)
case error(Error)}5. AppDelegate.swift
import UIKitlet themeColor = UIColor(red: 0.01, green: 0.41, blue: 0.22, alpha: 1.0)
@UIApplicationMainclass 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 UIKitfinal 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: - Privateprivate 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: - UITextFieldDelegateextension 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: - UICollectionViewDataSourceextension 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: - UICollectionViewDelegateFlowLayoutextension 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: - UICollectionViewDelegateextension 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: - UICollectionViewDragDelegateextension 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: - UICollectionViewDropDelegateextension 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 UIKitclass 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 UIKitclass FlickrPhotoHeaderView: UICollectionReusableView {
@IBOutlet weak var label: UILabel!
}后记
本篇主要讲述了UICollectionView的重用、选择和重排序,感兴趣的给个赞或者关注~~~
作者:刀客传奇
链接:https://www.jianshu.com/p/6c20e218e16d