美文网首页iOS 实用
UIKit框架(十七) —— 基于自定义UICollection

UIKit框架(十七) —— 基于自定义UICollection

作者: 刀客传奇 | 来源:发表于2019-04-27 16:28 被阅读33次

    版本记录

    版本号 时间
    V1.0 2019.04.27 星期六

    前言

    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的重用、选择和重排序(一)
    12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
    13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
    14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
    15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
    16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)

    源码

    1. Swift

    首先看下代码组织结构

    接着看一下sb文件

    1. CustomLayoutAttributes.swift
    
    import UIKit
    
    final class CustomLayoutAttributes: UICollectionViewLayoutAttributes {
    
      // MARK: - Properties
      var parallax: CGAffineTransform = .identity
      var initialOrigin: CGPoint = .zero
      var headerOverlayAlpha = CGFloat(0)
    
      // MARK: - Life Cycle
      override func copy(with zone: NSZone?) -> Any {
        guard let copiedAttributes = super.copy(with: zone) as? CustomLayoutAttributes else {
          return super.copy(with: zone)
        }
    
        copiedAttributes.parallax = parallax
        copiedAttributes.initialOrigin = initialOrigin
        copiedAttributes.headerOverlayAlpha = headerOverlayAlpha
        return copiedAttributes
      }
      
      override func isEqual(_ object: Any?) -> Bool {
        guard let otherAttributes = object as? CustomLayoutAttributes else {
          return false
        }
    
        if NSValue(cgAffineTransform: otherAttributes.parallax) != NSValue(cgAffineTransform: parallax)
          || otherAttributes.initialOrigin != initialOrigin
          || otherAttributes.headerOverlayAlpha != headerOverlayAlpha {
            return false
        }
    
        return super.isEqual(object)
      }
    }
    
    2. CustomLayoutSettings.swift
    
    import UIKit
    
    struct CustomLayoutSettings {
    
      // Elements sizes
      var itemSize: CGSize?
      var headerSize: CGSize?
      var menuSize: CGSize?
      var sectionsHeaderSize: CGSize?
      var sectionsFooterSize: CGSize?
    
      // Behaviours
      var isHeaderStretchy: Bool
      var isAlphaOnHeaderActive: Bool
      var headerOverlayMaxAlphaValue: CGFloat
      var isMenuSticky: Bool
      var isSectionHeadersSticky: Bool
      var isParallaxOnCellsEnabled: Bool
    
      // Spacing
      var minimumInteritemSpacing: CGFloat
      var minimumLineSpacing: CGFloat
      var maxParallaxOffset: CGFloat
    }
    
    extension CustomLayoutSettings {
    
      init() {
        self.itemSize = nil
        self.headerSize = nil
        self.menuSize = nil
        self.sectionsHeaderSize = nil
        self.sectionsFooterSize = nil
        self.isHeaderStretchy = false
        self.isAlphaOnHeaderActive = true
        self.headerOverlayMaxAlphaValue = 0
        self.isMenuSticky = false
        self.isSectionHeadersSticky = false
        self.isParallaxOnCellsEnabled = false
        self.maxParallaxOffset = 0
        self.minimumInteritemSpacing = 0
        self.minimumLineSpacing = 0
      }
    }
    
    3. CustomLayout.swift
    
    import UIKit
    
    final class CustomLayout: UICollectionViewLayout {
      
      enum Element: String {
        case header
        case menu
        case sectionHeader
        case sectionFooter
        case cell
        
        var id: String {
          return self.rawValue
        }
        
        var kind: String {
          return "Kind\(self.rawValue.capitalized)"
        }
      }
      
      override public class var layoutAttributesClass: AnyClass {
        return CustomLayoutAttributes.self
      }
      
      override public var collectionViewContentSize: CGSize {
        return CGSize(width: collectionViewWidth, height: contentHeight)
      }
      
      // MARK: - Properties
      var settings = CustomLayoutSettings()
      private var oldBounds = CGRect.zero
      private var contentHeight = CGFloat()
      private var cache = [Element: [IndexPath: CustomLayoutAttributes]]()
      private var visibleLayoutAttributes = [CustomLayoutAttributes]()
      private var zIndex = 0
      
      private var collectionViewHeight: CGFloat {
        return collectionView!.frame.height
      }
      
      private var collectionViewWidth: CGFloat {
        return collectionView!.frame.width
      }
      
      private var cellHeight: CGFloat {
        guard let itemSize = settings.itemSize else {
          return collectionViewHeight
        }
        
        return itemSize.height
      }
      
      private var cellWidth: CGFloat {
        guard let itemSize = settings.itemSize else {
          return collectionViewWidth
        }
        
        return itemSize.width
      }
      
      private var headerSize: CGSize {
        guard let headerSize = settings.headerSize else {
          return .zero
        }
        
        return headerSize
      }
      
      private var menuSize: CGSize {
        guard let menuSize = settings.menuSize else {
          return .zero
        }
        
        return menuSize
      }
      
      private var sectionsHeaderSize: CGSize {
        guard let sectionsHeaderSize = settings.sectionsHeaderSize else {
          return .zero
        }
        
        return sectionsHeaderSize
      }
      
      private var sectionsFooterSize: CGSize {
        guard let sectionsFooterSize = settings.sectionsFooterSize else {
          return .zero
        }
        
        return sectionsFooterSize
      }
      
      private var contentOffset: CGPoint {
        return collectionView!.contentOffset
      }
    }
    
    // MARK: - LAYOUT CORE PROCESS
    extension CustomLayout {
      
      override public func prepare() {
        guard let collectionView = collectionView,
          cache.isEmpty else {
            return
        }
        
        prepareCache()
        contentHeight = 0
        zIndex = 0
        oldBounds = collectionView.bounds
        let itemSize = CGSize(width: cellWidth, height: cellHeight)
        
        let headerAttributes = CustomLayoutAttributes(
          forSupplementaryViewOfKind: Element.header.kind,
          with: IndexPath(item: 0, section: 0)
        )
        prepareElement(size: headerSize, type: .header, attributes: headerAttributes)
        
        let menuAttributes = CustomLayoutAttributes(
          forSupplementaryViewOfKind: Element.menu.kind,
          with: IndexPath(item: 0, section: 0))
        prepareElement(size: menuSize, type: .menu, attributes: menuAttributes)
        
        for section in 0 ..< collectionView.numberOfSections {
          
          let sectionHeaderAttributes = CustomLayoutAttributes(
            forSupplementaryViewOfKind: UICollectionElementKindSectionHeader,
            with: IndexPath(item: 0, section: section))
          prepareElement(
            size: sectionsHeaderSize,
            type: .sectionHeader,
            attributes: sectionHeaderAttributes)
          
          for item in 0 ..< collectionView.numberOfItems(inSection: section) {
            let cellIndexPath = IndexPath(item: item, section: section)
            let attributes = CustomLayoutAttributes(forCellWith: cellIndexPath)
            let lineInterSpace = settings.minimumLineSpacing
            attributes.frame = CGRect(
              x: 0 + settings.minimumInteritemSpacing,
              y: contentHeight + lineInterSpace,
              width: itemSize.width,
              height: itemSize.height
            )
            attributes.zIndex = zIndex
            contentHeight = attributes.frame.maxY
            cache[.cell]?[cellIndexPath] = attributes
            zIndex += 1
          }
          
          let sectionFooterAttributes = CustomLayoutAttributes(
            forSupplementaryViewOfKind: UICollectionElementKindSectionFooter,
            with: IndexPath(item: 1, section: section))
          prepareElement(
            size: sectionsFooterSize,
            type: .sectionFooter,
            attributes: sectionFooterAttributes)
        }
        
        updateZIndexes()
      }
      
      override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        if oldBounds.size != newBounds.size {
          cache.removeAll(keepingCapacity: true)
        }
        return true
      }
      
      private func prepareCache() {
        cache.removeAll(keepingCapacity: true)
        cache[.header] = [IndexPath: CustomLayoutAttributes]()
        cache[.menu] = [IndexPath: CustomLayoutAttributes]()
        cache[.sectionHeader] = [IndexPath: CustomLayoutAttributes]()
        cache[.sectionFooter] = [IndexPath: CustomLayoutAttributes]()
        cache[.cell] = [IndexPath: CustomLayoutAttributes]()
      }
      
      private func prepareElement(size: CGSize, type: Element, attributes: CustomLayoutAttributes) {
        guard size != .zero else { return }
        
        attributes.initialOrigin = CGPoint(x: 0, y: contentHeight)
        attributes.frame = CGRect(origin: attributes.initialOrigin, size: size)
        
        attributes.zIndex = zIndex
        zIndex += 1
        
        contentHeight = attributes.frame.maxY
        
        cache[type]?[attributes.indexPath] = attributes
      }
      
      private func updateZIndexes(){
        guard let sectionHeaders = cache[.sectionHeader] else { return }
        
        var sectionHeadersZIndex = zIndex
        for (_, attributes) in sectionHeaders {
          attributes.zIndex = sectionHeadersZIndex
          sectionHeadersZIndex += 1
        }
        
        cache[.menu]?.first?.value.zIndex = sectionHeadersZIndex
      }
    }
    
    //MARK: - PROVIDING ATTRIBUTES TO THE COLLECTIONVIEW
    extension CustomLayout {
      
      public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        switch elementKind {
        case UICollectionElementKindSectionHeader:
          return cache[.sectionHeader]?[indexPath]
          
        case UICollectionElementKindSectionFooter:
          return cache[.sectionFooter]?[indexPath]
          
        case Element.header.kind:
          return cache[.header]?[indexPath]
          
        default:
          return cache[.menu]?[indexPath]
        }
      }
      
      override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache[.cell]?[indexPath]
      }
      
      override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let collectionView = collectionView else { return nil }
        
        visibleLayoutAttributes.removeAll(keepingCapacity: true)
        
        let halfHeight = collectionViewHeight * 0.5
        let halfCellHeight = cellHeight * 0.5
        
        for (type, elementInfos) in cache {
          for (indexPath, attributes) in elementInfos {
            
            attributes.parallax = .identity
            attributes.transform = .identity
            
            updateSupplementaryViews(type, attributes: attributes, collectionView: collectionView, indexPath: indexPath)
            if attributes.frame.intersects(rect) {
              if type == .cell,
                settings.isParallaxOnCellsEnabled {
                updateCells(attributes, halfHeight: halfHeight, halfCellHeight: halfCellHeight)
              }
              visibleLayoutAttributes.append(attributes)
            }
          }
        }
        return visibleLayoutAttributes
      }
      
      private func updateSupplementaryViews(_ type: Element, attributes: CustomLayoutAttributes, collectionView: UICollectionView, indexPath: IndexPath) {
        if type == .sectionHeader,
          settings.isSectionHeadersSticky {
          
          let upperLimit = CGFloat(collectionView.numberOfItems(inSection: indexPath.section)) * (cellHeight + settings.minimumLineSpacing)
          let menuOffset = settings.isMenuSticky ? menuSize.height : 0
          attributes.transform =  CGAffineTransform(
            translationX: 0,
            y: min(upperLimit, max(0, contentOffset.y - attributes.initialOrigin.y + menuOffset)))
          
        } else if type == .header,
          settings.isHeaderStretchy {
          
          let updatedHeight = min(
            collectionView.frame.height,
            max(headerSize.height, headerSize.height - contentOffset.y))
          
          let scaleFactor = updatedHeight / headerSize.height
          let delta = (updatedHeight - headerSize.height) / 2
          let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
          let translation = CGAffineTransform(translationX: 0, y: min(contentOffset.y, headerSize.height) + delta)
          attributes.transform = scale.concatenating(translation)
          if settings.isAlphaOnHeaderActive {
            attributes.headerOverlayAlpha = min(settings.headerOverlayMaxAlphaValue, contentOffset.y / headerSize.height)
          }
          
        } else if type == .menu,
          settings.isMenuSticky {
          
          attributes.transform = CGAffineTransform(translationX: 0, y: max(attributes.initialOrigin.y, contentOffset.y) - headerSize.height)
        }
      }
      
      private func updateCells(_ attributes: CustomLayoutAttributes, halfHeight: CGFloat, halfCellHeight: CGFloat) {
        let cellDistanceFromCenter = attributes.center.y - contentOffset.y - halfHeight
        let parallaxOffset = -(settings.maxParallaxOffset * cellDistanceFromCenter) / (halfHeight + halfCellHeight)
        let boundedParallaxOffset = min(max(-settings.maxParallaxOffset, parallaxOffset), settings.maxParallaxOffset)
        attributes.parallax = CGAffineTransform(translationX: 0, y: boundedParallaxOffset)
      }
    }
    
    4. MockDataManager.swift
    
    protocol Team {
      var marks: [String] { get }
      var playerPictures: [[String]] { get }
    }
    
    struct Owls: Team {
      let marks = ["4/5", "3/5", "4/5", "2/5"]
      let playerPictures = [
        ["Owls-goalkeeper"],
        ["Owls-d1", "Owls-d2", "Owls-d3", "Owls-d4"],
        ["Owls-m1", "Owls-m2", "Owls-m3", "Owls-m4"],
        ["Owls-f1", "Owls-f2"]
      ]
    }
    
    struct Tigers: Team {
      let marks = ["1/5", "3/5", "3/5", "5/5"]
      let playerPictures = [
        ["Tigers-goalkeeper"],
        ["Tigers-d1", "Tigers-d2", "Tigers-d3", "Tigers-d4"],
        ["Tigers-m1", "Tigers-m2", "Tigers-m3", "Tigers-m4"],
        ["Tigers-f1", "Tigers-f2"]
      ]
    }
    
    struct Parrots: Team {
      let marks = ["3/5", "2/5", "4/5", "5/5"]
      let playerPictures = [
        ["Parrots-goalkeeper"],
        ["Parrots-d1", "Parrots-d2", "Parrots-d3", "Parrots-d4"],
        ["Parrots-m1", "Parrots-m2", "Parrots-m3", "Parrots-m4"],
        ["Parrots-f1", "Parrots-f2"]
      ]
    }
    
    struct Giraffes: Team {
      let marks = ["5/5", "4/5", "3/5", "1/5"]
      let playerPictures = [
        ["Giraffes-goalkeeper"],
        ["Giraffes-d1", "Giraffes-d2", "Giraffes-d3", "Giraffes-d4"],
        ["Giraffes-m1", "Giraffes-m2", "Giraffes-m3", "Giraffes-m4"],
        ["Giraffes-f1", "Giraffes-f2"]
      ]
    }
    
    5. HeaderView.swift
    
    import UIKit
    
    final class HeaderView: UICollectionReusableView {
    
      // MARK: - IBOutlets
      @IBOutlet weak var overlayView: UIView!
    
      // MARK: - Life Cycle
      open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        super.apply(layoutAttributes)
    
        guard let customFlowLayoutAttributes = layoutAttributes as? CustomLayoutAttributes else {
          return
        }
    
        overlayView?.alpha = customFlowLayoutAttributes.headerOverlayAlpha
      }
    }
    
    6. MenuView.swift
    
    import UIKit
    
    protocol MenuViewDelegate {
      func reloadCollectionViewDataWithTeamIndex(_ index: Int)
    }
    
    final class MenuView: UICollectionReusableView {
    
      // MARK: - Properties
      var delegate: MenuViewDelegate?
      
      // MARK: - View Life Cycle
      override func prepareForReuse() {
        super.prepareForReuse()
    
        delegate = nil
      }
    }
    
    // MARK: - IBActions
    extension MenuView {
    
      @IBAction func tappedButton(_ sender: UIButton) {
        delegate?.reloadCollectionViewDataWithTeamIndex(sender.tag)
      }
    }
    
    7. SectionHeaderView.swift
    
    import UIKit
    
    final class SectionHeaderView: UICollectionReusableView {
    
      // MARK: - IBOutlets
      @IBOutlet weak var title: UILabel!
    }
    
    8. PlayerCell.swift
    
    import UIKit
    
    final class PlayerCell: UICollectionViewCell {
    
      // MARK: - IBOutlets
      @IBOutlet weak var picture: UIImageView!
    
      // MARK: - View Life Cycle
      override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        super.apply(layoutAttributes)
    
        guard let attributes = layoutAttributes as? CustomLayoutAttributes else {
          return
        }
    
        picture.transform = attributes.parallax
      }
      
      override func prepareForReuse() {
        super.prepareForReuse()
    
        picture.transform = .identity
      }
    }
    
    9. SectionFooterView.swift
    
    import UIKit
    
    final class SectionFooterView: UICollectionReusableView {
    
      // MARK: - IBOutlets
      @IBOutlet weak var mark: UILabel!
    }
    
    10. JungleCupCollectionViewController.swift
    
    import UIKit
    
    final class JungleCupCollectionViewController: UICollectionViewController {
     
      // MARK: - Properties
      var customLayout: CustomLayout? {
        return collectionView?.collectionViewLayout as? CustomLayout
      }
    
      private let teams: [Team] = [Owls(), Giraffes(), Parrots(), Tigers()]
      private let sections = ["Goalkeeper", "Defenders", "Midfielders", "Forwards"]
      private var displayedTeam = 0
    
      override var prefersStatusBarHidden: Bool {
        return true
      }
    
      // MARK: - View Life Cycle
      override func viewDidLoad() {
        super.viewDidLoad()
    
        setupCollectionViewLayout()
      }
    }
    
    private extension JungleCupCollectionViewController {
    
      func setupCollectionViewLayout() {
        guard let collectionView = collectionView, let customLayout = customLayout else { return }
    
        collectionView.register(
          UINib(nibName: "HeaderView", bundle: nil),
          forSupplementaryViewOfKind: CustomLayout.Element.header.kind,
          withReuseIdentifier: CustomLayout.Element.header.id
        )
    
        collectionView.register(
          UINib(nibName: "MenuView", bundle: nil),
          forSupplementaryViewOfKind: CustomLayout.Element.menu.kind,
          withReuseIdentifier: CustomLayout.Element.menu.id
        )
    
        customLayout.settings.itemSize = CGSize(width: collectionView.frame.width, height: 200)
        customLayout.settings.headerSize = CGSize(width: collectionView.frame.width, height: 300)
        customLayout.settings.menuSize = CGSize(width: collectionView.frame.width, height: 70)
        customLayout.settings.sectionsHeaderSize = CGSize(width: collectionView.frame.width, height: 50)
        customLayout.settings.sectionsFooterSize = CGSize(width: collectionView.frame.width, height: 50)
        customLayout.settings.isHeaderStretchy = true
        customLayout.settings.isAlphaOnHeaderActive = true
        customLayout.settings.headerOverlayMaxAlphaValue = CGFloat(0.6)
        customLayout.settings.isMenuSticky = true
        customLayout.settings.isSectionHeadersSticky = true
        customLayout.settings.isParallaxOnCellsEnabled = true
        customLayout.settings.maxParallaxOffset = 60
        customLayout.settings.minimumInteritemSpacing = 0
        customLayout.settings.minimumLineSpacing = 3
      }
    }
    
    //MARK: - UICollectionViewDataSource
    extension JungleCupCollectionViewController {
    
      override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return sections.count
      }
    
      override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return teams[displayedTeam].playerPictures[section].count
      }
    
      override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomLayout.Element.cell.id, for: indexPath)
        if let playerCell = cell as? PlayerCell {
          playerCell.picture.image = UIImage(named: teams[displayedTeam].playerPictures[indexPath.section][indexPath.item])
        }
        return cell
      }
    
      override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case UICollectionElementKindSectionHeader:
          let supplementaryView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CustomLayout.Element.sectionHeader.id, for: indexPath)
          if let sectionHeaderView = supplementaryView as? SectionHeaderView {
            sectionHeaderView.title.text = sections[indexPath.section]
          }
          return supplementaryView
    
        case UICollectionElementKindSectionFooter:
          let supplementaryView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CustomLayout.Element.sectionFooter.id, for: indexPath)
          if let sectionFooterView = supplementaryView as? SectionFooterView {
            sectionFooterView.mark.text = "Strength: \(teams[displayedTeam].marks[indexPath.section])"
          }
          return supplementaryView
    
        case CustomLayout.Element.header.kind:
          let topHeaderView = collectionView.dequeueReusableSupplementaryView(
            ofKind: kind,
            withReuseIdentifier: CustomLayout.Element.header.id,
            for: indexPath
          )
          return topHeaderView
    
        case CustomLayout.Element.menu.kind:
          let menuView = collectionView.dequeueReusableSupplementaryView(
            ofKind: kind,
            withReuseIdentifier: CustomLayout.Element.menu.id,
            for: indexPath
          )
          if let menuView = menuView as? MenuView {
            menuView.delegate = self
          }
          return menuView
    
        default:
          fatalError("Unexpected element kind")
        }
      }
    }
    
    // MARK: - MenuViewDelegate
    extension JungleCupCollectionViewController: MenuViewDelegate {
    
      func reloadCollectionViewDataWithTeamIndex(_ index: Int) {
        displayedTeam = index
        collectionView?.reloadData()
      }
    }
    

    后记

    本篇主要讲述了基于自定义UICollectionViewLayout布局的简单示例,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

        本文标题:UIKit框架(十七) —— 基于自定义UICollection

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