美文网首页傲视苍穹iOS《Objective-C》VIP专题
UIKit框架(十二) —— UICollectionView的

UIKit框架(十二) —— UICollectionView的

作者: 刀客传奇 | 来源:发表于2019-01-10 12:07 被阅读46次

    版本记录

    版本号 时间
    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的重用、选择和重排序,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

        本文标题:UIKit框架(十二) —— UICollectionView的

        本文链接:https://www.haomeiwen.com/subject/ykgorqtx.html