iglistkit

IGListKit框架详细解析(四) —— 基于IGListKi

2019-01-19  本文已影响90人  刀客传奇

版本记录

版本号 时间
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简单示例,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读