IGListKit框架详细解析(四) —— 基于IGListKit框架的更好的UICollectionViews简单示例(三)

版本记录

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

前言

IGListKit这个框架可能很多人没有听过,它其实就是一个数据驱动的UICollectionView框架,用于构建快速灵活的列表。它由Instagram开发,接下来这几篇我们就一起看一下这个框架。感兴趣的看上面几篇。
1. IGListKit框架详细解析(一) —— 基本概览(一)
2. IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)
3. IGListKit框架详细解析(三) —— 基于IGListKit框架的更好的UICollectionViews简单示例(二)

源码

1. Swift

首先看下工程组织结构

接下来就是代码了

1. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.backgroundColor = .black
    let nav = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)
    nav.pushViewController(FeedViewController(), animated: false)
    window?.rootViewController = nav
    window?.makeKeyAndVisible()
    return true
  }
}
2. DateSortable.swift
import Foundation

protocol DateSortable {
  var date: Date { get }
}
3. JournalEntry.swift
import Foundation

class JournalEntry: NSObject, DateSortable {
  let date: Date
  let text: String
  let user: User
  
  init(date: Date, text: String, user: User) {
    self.date = date
    self.text = text
    self.user = user
  }
}
4. Message.swift
import UIKit

class Message: NSObject, DateSortable {
  let date: Date
  let text: String
  let user: User
  
  init(date: Date, text: String, user: User) {
    self.date = date
    self.text = text
    self.user = user
  }
}
5. SolFormatter.swift
import Foundation

struct SolFormatter {
  let landingDate: Date
  
  init(landingDate: Date = Date(timeIntervalSinceNow: -31725960)) {
    self.landingDate = landingDate
  }
  
  func sols(fromDate date: Date) -> Int {
    let martianDay: TimeInterval = 1477 * 60 // 24h37m
    let seconds = date.timeIntervalSince(landingDate)
    return lround(seconds / martianDay)
  }
}
6. User.swift
import Foundation

class User: NSObject {
  let id: Int
  let name: String
  
  init(id: Int, name: String) {
    self.id = id
    self.name = name
  }
}
7. Weather.swift
import UIKit

enum WeatherCondition: String {
  case cloudy = "Cloudy"
  case sunny = "Sunny"
  case partlyCloudy = "Partly Cloudy"
  case dustStorm = "Dust Storm"
  
  var emoji: String {
    switch self {
    case .cloudy: return "☁️"
    case .sunny: return "☀️"
    case .partlyCloudy: return "⛅️"
    case .dustStorm: return "🌪"
    }
  }
}

class Weather: NSObject {
  let temperature: Int
  let high: Int
  let low: Int
  let date: Date
  let sunrise: String
  let sunset: String
  let condition: WeatherCondition
  
  init(
    temperature: Int,
    high: Int,
    low: Int,
    date: Date,
    sunrise: String,
    sunset: String,
    condition: WeatherCondition
    ) {
    self.temperature = temperature
    self.high = high
    self.low = low
    self.date = date
    self.sunrise = sunrise
    self.sunset = sunset
    self.condition = condition
  }
}
8. NSObject+ListDiffable.swift
import Foundation
import IGListKit

// MARK: - ListDiffable
extension NSObject: ListDiffable {
  public func diffIdentifier() -> NSObjectProtocol {
    return self
  }

  public func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    return isEqual(object)
  }
}
9. JournalSectionController.swift
import IGListKit

class JournalSectionController: ListSectionController {
  var entry: JournalEntry!
  let solFormatter = SolFormatter()

  override init() {
    super.init()
    inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
  }

}

// MARK: - Data Provider
extension JournalSectionController {
  override func numberOfItems() -> Int {
    return 2
  }
  
  override func sizeForItem(at index: Int) -> CGSize {
    guard
      let context = collectionContext,
      let entry = entry
      else {
        return .zero
    }
    let width = context.containerSize.width
    
    if index == 0 {
      return CGSize(width: width, height: 30)
    } else {
      return JournalEntryCell.cellSize(width: width, text: entry.text)
    }

  }
  
  override func cellForItem(at index: Int) -> UICollectionViewCell {
    let cellClass: AnyClass = index == 0 ? JournalEntryDateCell.self : JournalEntryCell.self
    let cell = collectionContext!.dequeueReusableCell(of: cellClass, for: self, at: index)
    
    if let cell = cell as? JournalEntryDateCell {
      cell.label.text = "SOL \(solFormatter.sols(fromDate: entry.date))"
    } else if let cell = cell as? JournalEntryCell {
      cell.label.text = entry.text
    }
    return cell

  }
  
  override func didUpdate(to object: Any) {
    entry = object as? JournalEntry
  }
}
10. MessageSectionController.swift
import IGListKit

class MessageSectionController: ListSectionController {
  var message: Message!
  
  override init() {
    super.init()
    inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
  }
}

// MARK: - Data Provider
extension MessageSectionController {
  override func numberOfItems() -> Int {
    return 1
  }
  
  override func sizeForItem(at index: Int) -> CGSize {
    guard
      let context = collectionContext,
      let message = message
      else {
        return .zero
    }
    return MessageCell.cellSize(width: context.containerSize.width, text: message.text)
  }
  
  override func cellForItem(at index: Int) -> UICollectionViewCell {
    let cell = collectionContext?.dequeueReusableCell(of: MessageCell.self, for: self, at: index) as! MessageCell
    cell.messageLabel.text = message.text
    cell.titleLabel.text = message.user.name.uppercased()
    
    return cell
  }
  
  override func didUpdate(to object: Any) {
    message = object as? Message
  }
}
11. WeatherSectionController.swift
import IGListKit

class WeatherSectionController: ListSectionController {
  var weather: Weather!
  var expanded = false
  
  override init() {
    super.init()
    inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
  }
}

// MARK: - Data Provider
extension WeatherSectionController {
  override func didUpdate(to object: Any) {
    weather = object as? Weather
  }
  
  override func numberOfItems() -> Int {
    return expanded ? 5 : 1
  }
  
  override func sizeForItem(at index: Int) -> CGSize {
    guard let context = collectionContext else {
      return .zero
    }
    let width = context.containerSize.width
    if index == 0 {
      return CGSize(width: width, height: 70)
    } else {
      return CGSize(width: width, height: 40)
    }
  }
  
  override func cellForItem(at index: Int) -> UICollectionViewCell {
    let cellClass: AnyClass = index == 0 ? WeatherSummaryCell.self : WeatherDetailCell.self
    let cell = collectionContext!.dequeueReusableCell(of: cellClass, for: self, at: index)
    
    if let cell = cell as? WeatherSummaryCell {
      cell.setExpanded(expanded)
    } else if let cell = cell as? WeatherDetailCell {
      let title: String, detail: String
      switch index {
      case 1:
        title = "SUNRISE"
        detail = weather.sunrise
      case 2:
        title = "SUNSET"
        detail = weather.sunset
      case 3:
        title = "HIGH"
        detail = "\(weather.high) C"
      case 4:
        title = "LOW"
        detail = "\(weather.low) C"
      default:
        title = "n/a"
        detail = "n/a"
      }
      cell.titleLabel.text = title
      cell.detailLabel.text = detail
    }
    return cell
  }
  
  override func didSelectItem(at index: Int) {
    collectionContext?.performBatch(animated: true, updates: { batchContext in
      self.expanded.toggle()
      batchContext.reload(self)
    }, completion: nil)
  }
}
12. JournalLoader.swift
import Foundation

class JournalEntryLoader {
  var entries: [JournalEntry] = []
  
  func loadLatest() {
    let user = User(id: 1, name: "Mark Watney")
    let entries = [
      JournalEntry(
        date: Date(timeIntervalSinceNow: -1727283),
        text: "Ok I think I have this potato thing figured out. I'm using some of the leftover fuel from the landing thruster and basically lighting it on fire. The hydrogen and oxygen combine to make water. If I throttle the reaction I can let this run all day and generate enough water in the air to hydrate my potatos.\n\nThough, I'm basically igniting jet fuel in my living room.",
        user: user
      ),
      JournalEntry(
        date: Date(timeIntervalSinceNow: -1382400),
        text: "I blew up.\n\nMy potato hydration system was working perfectly, but I forgot to account for excess oxygen from the reaction. I ended up with 30% pure oxygen in the HAB. Where I'm making mini explosions. Oh did I mention I live here?\n\nI survived but the HAB is basically gone, along with all my potatos. The cold air instantly froze the ones I have, so there's that at least.",
        user: user
      ),
      JournalEntry(
        date: Date(timeIntervalSinceNow: -823200),
        text: "I figured out how to communicate with NASA! Years ago we sent a small probe called Pathfinder to Mars to poke at the sand a bit. The little rover only lasted a couple months, but I found it! All I had to do was swap the batteries and its as good as new.\n\nWith all this in place I can send pictures to NASA, maybe Johansen can tell me how to hack this thing?",
        user: user
      ),
      JournalEntry(
        date: Date(timeIntervalSinceNow: -259200),
        text: "Alright, its time for me to leave the HAB and make the several-thousand kilometer trek to the next landing site. The MAV is already there, so I'm going to try to launch this thing and intercept with Hermes. Sounds crazy, right?\n\nBut it's the last chance I've got.",
        user: user
      )
    ]
    self.entries = entries
  }
}
13. Pathfinder.swift
import Foundation

protocol PathfinderDelegate: class {
  func pathfinderDidUpdateMessages(pathfinder: Pathfinder)
}

private func delay(time: Double = 1, execute work: @escaping @convention(block) () -> Swift.Void) {
  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + time) {
    work()
  }
}

private func lewisMessage(text: String, interval: TimeInterval = 0) -> Message {
  let user = User(id: 2, name: "cpt.lewis")
  return Message(date: Date(timeIntervalSinceNow: interval), text: text, user: user)
}

class Pathfinder {
  weak var delegate: PathfinderDelegate?
  
  var messages: [Message] = {
    var arr: [Message] = []
    arr.append(lewisMessage(text: "Mark, are you receiving me?", interval: -803200))
    arr.append(lewisMessage(text: "I think I left behind some ABBA, might help with the drive 😜", interval: -259200))
    return arr
  }() {
    didSet {
      delegate?.pathfinderDidUpdateMessages(pathfinder: self)
    }
  }
  
  func connect() {
    delay(time: 2.3) {
      self.messages.append(lewisMessage(text: "Liftoff in 3..."))
      delay {
        self.messages.append(lewisMessage(text: "2..."))
        delay {
          self.messages.append(lewisMessage(text: "1..."))
        }
      }
    }
  }
}
14. TextSize.swift
import UIKit

public struct TextSize {
  private struct CacheEntry: Hashable, Equatable {
    let text: String
    let font: UIFont
    let width: CGFloat
    let insets: UIEdgeInsets
    
    func hash(into hasher: inout Hasher) {
      hasher.combine(text)
      hasher.combine(width)
      hasher.combine(insets.top)
      hasher.combine(insets.left)
      hasher.combine(insets.bottom)
      hasher.combine(insets.right)
    }
    
    static func ==(lhs: TextSize.CacheEntry, rhs: TextSize.CacheEntry) -> Bool {
      return lhs.width == rhs.width && lhs.insets == rhs.insets && lhs.text == rhs.text
    }
  }
  
  private static var cache: [CacheEntry: CGRect] = [:] {
    didSet {
      assert(Thread.isMainThread)
    }
  }
  
  public static func size(_ text: String, font: UIFont, width: CGFloat, insets: UIEdgeInsets = .zero) -> CGRect {
    let key = CacheEntry(text: text, font: font, width: width, insets: insets)
    if let hit = cache[key] {
      return hit
    }
    
    let constrainedSize = CGSize(width: width - insets.left - insets.right, height: .greatestFiniteMagnitude)
    let attributes = [NSAttributedString.Key.font: font]
    let options: NSStringDrawingOptions = [.usesFontLeading, .usesLineFragmentOrigin]
    var bounds = (text as NSString).boundingRect(with: constrainedSize, options: options, attributes: attributes, context: nil)
    bounds.size.width = width
    bounds.size.height = ceil(bounds.height + insets.top + insets.bottom)
    cache[key] = bounds
    return bounds
  }
}
15. Theme.swift
import UIKit

extension UIColor {
  // https://github.com/yeahdongcn/UIColor-Hex-Swift/blob/master/HEXColor/UIColorExtension.swift
  public convenience init(hex6: UInt32, alpha: CGFloat = 1) {
    let divisor = CGFloat(255)
    let red     = CGFloat((hex6 & 0xFF0000) >> 16) / divisor
    let green   = CGFloat((hex6 & 0x00FF00) >>  8) / divisor
    let blue    = CGFloat( hex6 & 0x0000FF       ) / divisor
    self.init(red: red, green: green, blue: blue, alpha: alpha)
  }
}

let CommonInsets = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15)

func AppFont(size: CGFloat = 18) -> UIFont {
  return UIFont(name: "OCRAStd", size: size)!
}
16. WxScanner.swift
import Foundation

class WxScanner {
  let currentWeather = Weather(
    temperature: 6,
    high: 13,
    low: -69,
    date: Date(),
    sunrise: "05:42",
    sunset: "17:58",
    condition: .dustStorm
  )
}
17. ClassicFeedViewController.swift
import UIKit

class ClassicFeedViewController: UIViewController {
  let loader = JournalEntryLoader()
  let solFormatter = SolFormatter()
  
  let collectionView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    layout.minimumLineSpacing = 0
    layout.minimumInteritemSpacing = 0
    layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
    let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
    view.backgroundColor = .black
    view.alwaysBounceVertical = true
    return view
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    collectionView.register(JournalEntryCell.self, forCellWithReuseIdentifier: "JournalEntryCell")
    collectionView.register(JournalEntryDateCell.self, forCellWithReuseIdentifier: "JournalEntryDateCell")
    collectionView.dataSource = self
    collectionView.delegate = self
    view.addSubview(collectionView)
    
    loader.loadLatest()
  }
  
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    collectionView.frame = view.bounds
  }
}

// MARK: - UICollectionViewDataSource
extension ClassicFeedViewController: UICollectionViewDataSource {
  func numberOfSections(in collectionView: UICollectionView) -> Int {
    return loader.entries.count
  }
  
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 2
  }
  
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let identifier = indexPath.item == 0 ? "JournalEntryDateCell" : "JournalEntryCell"
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
    let entry = loader.entries[indexPath.section]
    if let cell = cell as? JournalEntryDateCell {
      cell.label.text = "SOL \(solFormatter.sols(fromDate: entry.date))"
    } else if let cell = cell as? JournalEntryCell {
      cell.label.text = entry.text
    }
    return cell
  }
}

// MARK: - UICollectionViewDelegateFlowLayout
extension ClassicFeedViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let width = collectionView.bounds.width
    if indexPath.item == 0 {
      return CGSize(width: width, height: 30)
    } else {
      let entry = loader.entries[indexPath.section]
      return JournalEntryCell.cellSize(width: width, text: entry.text)
    }
  }
}
18. FeedViewController.swift
import UIKit
import IGListKit

class FeedViewController: UIViewController {
  let loader = JournalEntryLoader()
  let collectionView: UICollectionView = {
    let view = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
    view.backgroundColor = .black
    return view
  }()
  lazy var adapter: ListAdapter = {
    return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
  }()
  let pathfinder = Pathfinder()
  let wxScanner = WxScanner()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    loader.loadLatest()
    view.addSubview(collectionView)
    adapter.collectionView = collectionView
    adapter.dataSource = self
    pathfinder.delegate = self
    pathfinder.connect()
  }
  
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    collectionView.frame = view.bounds
  }
}

// MARK: - ListAdapterDataSource
extension FeedViewController: ListAdapterDataSource {
  func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    var items: [ListDiffable] = [wxScanner.currentWeather]
    items += loader.entries as [ListDiffable]
    items += pathfinder.messages as [ListDiffable]

    return items.sorted { (left: Any, right: Any) -> Bool in
      guard let left = left as? DateSortable, let right = right as? DateSortable else {
        return false
      }
      return left.date > right.date
    }
  }
  
  func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
    if object is Message {
      return MessageSectionController()
    } else if object is Weather {
      return WeatherSectionController()
    } else {
      return JournalSectionController()
    }
  }
  
  func emptyView(for listAdapter: ListAdapter) -> UIView? {
    return nil
  }
}

// MARK: - PathfinderDelegate
extension FeedViewController: PathfinderDelegate {
  func pathfinderDidUpdateMessages(pathfinder: Pathfinder) {
    adapter.performUpdates(animated: true)
  }
}
19. CustomNavigationBar.swift
import UIKit

class CustomNavigationBar: UINavigationBar {
  let titleLabel: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.text = "MARSLINK"
    label.font = AppFont()
    label.textAlignment = .center
    label.textColor = .white
    return label
  }()
  
  let statusLabel: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.text = "RECEIVING"
    label.font = AppFont(size: 13)
    label.textAlignment = .center
    label.textColor = UIColor(hex6: 0x42c84b)
    label.sizeToFit()
    return label
  }()
  
  let statusIndicator: CAShapeLayer = {
    let layer = CAShapeLayer()
    layer.strokeColor = UIColor.white.cgColor
    layer.lineWidth = 1
    layer.fillColor = UIColor.black.cgColor
    let size: CGFloat = 8
    let frame = CGRect(x: 0, y: 0, width: size, height: size)
    layer.path = UIBezierPath(roundedRect: frame, cornerRadius: size / 2).cgPath
    layer.frame = frame
    return layer
  }()
  
  let highlightLayer: CAShapeLayer = {
    let layer = CAShapeLayer()
    layer.fillColor = UIColor(hex6: 0x76879D).cgColor
    return layer
  }()

  var statusOn = false
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    layer.addSublayer(highlightLayer)
    layer.addSublayer(statusIndicator)
    addSubview(titleLabel)
    addSubview(statusLabel)
    barTintColor = .black
    updateStatus()
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func layoutSubviews() {
    super.layoutSubviews()
    let titleWidth: CGFloat = 130
    let borderHeight: CGFloat = 4
    
    let path = UIBezierPath()
    path.move(to: .zero)
    path.addLine(to: CGPoint(x: titleWidth, y: 0))
    path.addLine(to: CGPoint(x: titleWidth, y: bounds.height - borderHeight))
    path.addLine(to: CGPoint(x: bounds.width, y: bounds.height - borderHeight))
    path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
    path.addLine(to: CGPoint(x: 0, y: bounds.height))
    path.close()
    highlightLayer.path = path.cgPath
    
    titleLabel.frame = CGRect(x: 0, y: 0, width: titleWidth, height: bounds.height)
    statusLabel.frame = CGRect(
      x: bounds.width - statusLabel.bounds.width - CommonInsets.right,
      y: bounds.height - borderHeight - statusLabel.bounds.height - 6,
      width: statusLabel.bounds.width,
      height: statusLabel.bounds.height
    )
    statusIndicator.position = CGPoint(x: statusLabel.center.x - 50, y: statusLabel.center.y - 1)
  }
  
  func updateStatus() {
    statusOn.toggle()
    CATransaction.begin()
    CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
    statusIndicator.fillColor = (statusOn ? UIColor.white : UIColor.black).cgColor
    CATransaction.commit()
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6) {
      self.updateStatus()
    }
  }

}
20. JournalEntryCell.swift
import UIKit

class JournalEntryCell: UICollectionViewCell {
  static let font = AppFont()
  static let inset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15)
  
  static func cellSize(width: CGFloat, text: String) -> CGSize {
    return TextSize.size(text, font: JournalEntryCell.font, width: width, insets: JournalEntryCell.inset).size
  }
  
  let label: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.numberOfLines = 0
    label.font = JournalEntryCell.font
    label.textColor = .white
    return label
  }()
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    contentView.backgroundColor = UIColor(hex6: 0x0c1f3f)
    contentView.addSubview(label)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func layoutSubviews() {
    super.layoutSubviews()
    label.frame = bounds.inset(by: JournalEntryCell.inset)
  }
  
}
21. JournalEntryDateCell.swift
import UIKit

class JournalEntryDateCell: UICollectionViewCell {
  let label: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.font = AppFont(size: 14)
    label.textColor = UIColor(hex6: 0x42c84b)
    return label
  }()
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    contentView.backgroundColor = UIColor(hex6: 0x0c1f3f)
    contentView.addSubview(label)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func layoutSubviews() {
    super.layoutSubviews()
    let padding = CommonInsets
    label.frame = bounds.inset(by: UIEdgeInsets(top: 0, left: padding.left, bottom: 0, right: padding.right))
  }
}
22. MessageCell.swift
import UIKit

class MessageCell: UICollectionViewCell {
  static let titleHeight: CGFloat = 30
  static let font = AppFont()
  
  static func cellSize(width: CGFloat, text: String) -> CGSize {
    let labelBounds = TextSize.size(text, font: MessageCell.font, width: width, insets: CommonInsets)
    return CGSize(width: width, height: labelBounds.height + MessageCell.titleHeight)
  }
  
  let messageLabel: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.numberOfLines = 0
    label.font = MessageCell.font
    label.textColor = .white
    return label
  }()
  
  let titleLabel: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.font = AppFont(size: 14)
    label.textColor = UIColor(hex6: 0x42c84b)
    return label
  }()
  
  let statusLabel: UILabel = {
    let label = UILabel()
    label.layer.borderColor = UIColor(hex6: 0x76879d).cgColor
    label.layer.borderWidth = 1
    label.backgroundColor = .clear
    label.font = AppFont(size: 8)
    label.textColor = UIColor(hex6: 0x76879d)
    label.textAlignment = .center
    label.text = "NEW MESSAGE"
    return label
  }()
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    contentView.backgroundColor = UIColor(hex6: 0x0c1f3f)
    contentView.addSubview(messageLabel)
    contentView.addSubview(titleLabel)
    contentView.addSubview(statusLabel)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func layoutSubviews() {
    super.layoutSubviews()
    titleLabel.frame = CGRect(x: CommonInsets.left, y: 0, width: bounds.width - CommonInsets.left - CommonInsets.right, height: MessageCell.titleHeight)
    statusLabel.frame = CGRect(x: bounds.width - 80, y: 4, width: 70, height: 18)
    let messageFrame = CGRect(x: 0, y: titleLabel.frame.maxY, width: bounds.width, height: bounds.height - MessageCell.titleHeight)
    messageLabel.frame = messageFrame.inset(by: CommonInsets)
  }
}
23. WeatherDetailCell.swift
import UIKit

class WeatherDetailCell: UICollectionViewCell {
  let titleLabel: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.font = AppFont()
    label.textColor = UIColor(hex6: 0x42c84b)
    return label
  }()
  
  let detailLabel: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.font = AppFont()
    label.textColor = UIColor(hex6: 0x42c84b)
    label.textAlignment = .right
    return label
  }()
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    contentView.addSubview(titleLabel)
    contentView.addSubview(detailLabel)
    contentView.backgroundColor = UIColor(hex6: 0x0c1f3f)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func layoutSubviews() {
    super.layoutSubviews()
    let insetBounds = bounds.inset(by: CommonInsets)
    titleLabel.frame = insetBounds
    detailLabel.frame = insetBounds
  }
}
24. WeatherSummaryCell.swift
import UIKit

class WeatherSummaryCell: UICollectionViewCell {
  private let expandLabel: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.font = AppFont(size: 30)
    label.textColor = UIColor(hex6: 0x44758b)
    label.textAlignment = .center
    label.text = ">>"
    label.sizeToFit()
    return label
  }()
  
  let titleLabel: UILabel = {
    let label = UILabel()
    label.backgroundColor = .clear
    label.numberOfLines = 0
    
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.paragraphSpacing = 4
    let subtitleAttributes = [
      NSAttributedString.Key.font: AppFont(size: 14),
      NSAttributedString.Key.foregroundColor: UIColor(hex6: 0x42c84b),
      NSAttributedString.Key.paragraphStyle: paragraphStyle
    ]
    let titleAttributes = [
      NSAttributedString.Key.font: AppFont(size: 24),
      NSAttributedString.Key.foregroundColor: UIColor.white
    ]
    let attributedText = NSMutableAttributedString(string: "LATEST\n", attributes: subtitleAttributes)
    attributedText.append(NSAttributedString(string: "WEATHER", attributes: titleAttributes))
    label.attributedText = attributedText
    label.sizeToFit()
    
    return label
  }()
  
  func setExpanded(_ expanded: Bool) {
    expandLabel.transform = expanded ? CGAffineTransform(rotationAngle: CGFloat.pi / 2) : .identity
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    contentView.addSubview(expandLabel)
    contentView.addSubview(titleLabel)
    contentView.backgroundColor = UIColor(hex6: 0x0c1f3f)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func layoutSubviews() {
    super.layoutSubviews()
    let insets = CommonInsets
    titleLabel.frame = CGRect(x: insets.left, y: 0, width: titleLabel.bounds.width, height: bounds.height)
    expandLabel.center = CGPoint(x: bounds.width - expandLabel.bounds.width / 2 - insets.right, y: bounds.height / 2)
  }
}

后记

本篇主要简单介绍了基于IGListKit框架的更好的UICollectionViews简单示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容