理想中的代码

概览

一直在写代码,什么是理想中的代码

关于MVC

图片是《斯坦福大学公开课:iOS7应用开发》中第一节课中的图片,讲述了三者之间的关系。按照交通规则来看,黄色是不可逾越的,也就是说model和view之间不可以存在任何联系,controller可以访问和持有model和view,反之则不行。

那么问题来了,UICollectionViewCell,UITableViewCell属于什么呢?当然属于View,继承自它们的当然也属于View,为了方便,我们常常在某个cell中绑定一个model,然后在类里面根据model修改各个元素的值,我个人是不提倡这么做的。从单个页面来看,通常很便捷,但是从上面的图片来看,它已经违反了交通规则。

1、从单元测试来讲,提高了测试的耦合性,想要测试一个view必须先创建一个model

2、一旦绑定了一个类型的model将很难复用到其他界面相似的view,例如中国版的点赞列表、附近的人的列表、消息里面的关注列表,三者界面类似,数据model都是从user继承而来,又稍有不同,换句话说,需求可能是界面都不动,界面中的某个lable的text需要改变。另外如果一个view想要给其他业务或者其他团队或者开源出来给其他人用必须把model从cell中拿掉。这里可以举个开源的例子,com中聊天引用的一个项目JSQMessagesViewController 中的JSQMessagesCollectionViewCell

enum 枚举

enum Rank: Int {
    case ace = 1
    case two, three, four, five, six, seven, eight, nine, ten
    case jack, queen, king
    func simpleDescription() -> String {
        switch self {
        case .ace:
            return "ace"
        case .jack:
            return "jack"
        case .queen:
            return "queen"
        case .king:
            return "king"
        default:
            return String(self.rawValue)
        }
    }
}
let ace = Rank.ace
let aceRawValue = ace.rawValue

最初以为swift中的枚举只有以上添加了函数的操作,其实不然啊 看下面

/**
 This object gives specific change information about a collection.
 */
public enum CollectionChangeInformation: Equatable {

    /// This indicates that an element was updated at a specific index.
    case update(index: Int)

    /// This indicates that an element was deleted at a specific index.
    case delete(index: Int)

    /// This indicates that an element was inserted at a specific index.
    case insert(index: Int)
}

public func ==(lhs: CollectionChangeInformation, rhs: CollectionChangeInformation) -> Bool {
    switch (lhs, rhs) {
    case (.update(let l), .update(let r)):
        return l == r
    case (.delete(let l), .delete(let r)):
        return l == r
    case (.insert(let l), .insert(let r)):
        return l == r
    default:
        return false
    }
}

惊不惊喜,意外不?在枚举的枚举值里update发现了参数这是associated values,你可以理解为这些参数保存在了枚举的变量里面(You can think of the associated values as behaving like stored properties of the enumeration case instance),而且还遵守了Equatable协议,意味着可以自己定义怎么判断两个枚举值是否相等。来看看在苹果官方sdk中说了啥

///     extension StreetAddress: Equatable {
///         static func == (lhs: StreetAddress, rhs: StreetAddress) -> Bool {
///             return
///                 lhs.number == rhs.number &&
///                 lhs.street == rhs.street &&
///                 lhs.unit == rhs.unit
///         }
///     }

public protocol Equatable {

    /// Returns a Boolean value indicating whether two values are equal.
    ///
    /// Equality is the inverse of inequality. For any values `a` and `b`,
    /// `a == b` implies that `a != b` is `false`.
    ///
    /// - Parameters:
    ///   - lhs: A value to compare.
    ///   - rhs: Another value to compare.
    public static func ==(lhs: Self, rhs: Self) -> Bool
}
extension Equatable {

    /// Returns a Boolean value indicating whether two values are not equal.
    ///
    /// Inequality is the inverse of equality. For any values `a` and `b`, `a != b`
    /// implies that `a == b` is `false`.
    ///
    /// This is the default implementation of the not-equal-to operator (`!=`)
    /// for any type that conforms to `Equatable`.
    ///
    /// - Parameters:
    ///   - lhs: A value to compare.
    ///   - rhs: Another value to compare.
    public static func !=(lhs: Self, rhs: Self) -> Bool
}

Functional Reactive Programming(以下简称FRP)是一种响应变化的编程范式

我们之前一直在使用ReactiveCocoa 【ReactiveCocoa (RAC) is a Cocoa framework inspired by Functional Reactive Programming】,这两天又接触了RxSwift、RxCocoa,发现他们很类似,各有所长。之所以在这里说是因为看到RxCocoa在UI方面更加强大

/**
    Binds sequences of elements to collection view items.
    
    - parameter cellIdentifier: Identifier used to dequeue cells.
    - parameter source: Observable sequence of items.
    - parameter configureCell: Transform between sequence elements and view cells.
    - parameter cellType: Type of table view cell.
    - returns: Disposable object that can be used to unbind.
     
     Example

         let items = Observable.just([
             1,
             2,
             3
         ])

         items
             .bind(to: collectionView.rx.items(cellIdentifier: "Cell", cellType: NumberCell.self)) { (row, element, cell) in
                cell.value?.text = "\(element) @ \(row)"
             }
             .disposed(by: disposeBag)

        collectionView.rx.itemSelected.subscribe({ [weak self] indexPath in
            // do something 
        }).disposed(by: disposeBag)
*/

有没有很酷,哈哈 看起来不错哦上面例子中只是列举了最简单的一种看下面的复杂的

/**
    Binds sequences of elements to collection view items using a custom reactive data used to perform the transformation.
    
    - parameter dataSource: Data source used to transform elements to view cells.
    - parameter source: Observable sequence of items.
    - returns: Disposable object that can be used to unbind.
     
     Example
     
         let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, Double>>()

         let items = Observable.just([
             SectionModel(model: "First section", items: [
                 1.0,
                 2.0,
                 3.0
             ]),
             SectionModel(model: "Second section", items: [
                 1.0,
                 2.0,
                 3.0
             ]),
             SectionModel(model: "Third section", items: [
                 1.0,
                 2.0,
                 3.0
             ])
         ])

         dataSource.configureCell = { (dataSource, cv, indexPath, element) in
             let cell = cv.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! NumberCell
             cell.value?.text = "\(element) @ row \(indexPath.row)"
             return cell
         }

         items
            .bind(to: collectionView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
    */

MVVM

关于MVVM的介绍ReactiveViewModel,目前我们项目采用以下设计

model :仅仅是数据模型

view :这里的view包含所有的view和viewcontroller,其中view(包含cell和自定义view)仅仅是view,不包含任何model,另外viewcontroller仅仅负责Layout、Animations、Device rotation、View and window transitions、Presenting loaded UI

viewModel:所有的viewmodel会被viewcontroller持有,这里面负责处理页数调用PXApiMannager获取数据,并且保存在ViewModel的property里面,如果需要为cell准备数据,在这里进行加工

完整数据获取及刷新流程如下:

viewController(订阅ViewModel的某个Signal,如果有输出数据刷新view,如果有错误提示错误)——————>viewModel(调用PXApiManager获取某个signal,进行map加工操作,将数据自身持有)—————>PXApiManager(创建signal,并且调用AFNetworking获取相应数据,如果有有数据则通过Mantle转化为model并向创建的signal抛出,如果有错误也抛出,这里也会进行接口异常记录,链接请求统一处理header,统一添加token信息等等)

IGListKit

IGListKit是Facebook的又一神作,这里是raywenderlich上的一篇教程,教你如何快速的在list中添加功能,而且滑动起来非常的流畅。IGListKit非常智能,会自动检查你数据中的变化,并流畅的更新UICollectionView 中对应改变数据的部分。

总体分为五步:

1、声明一个IGListCollectionView的变量,并且添加到当前viewcontroller当中,并且设置布局

// 1
let collectionView: IGListCollectionView = {
  // 2
  let view = IGListCollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
  // 3
  view.backgroundColor = UIColor.black
  return view
}()

2、声明一个IGListAdapter的变量,并且为之设置view和databsource

lazy var adapter: IGListAdapter = {
  return IGListAdapter(updater: IGListAdapterUpdater(), viewController: self, workingRangeSize: 0)
}()

adapter.collectionView = collectionView
adapter.dataSource = self

3、在datasource中设置数据、IGListSectionController及为空时显示的view,注意⚠️这里的IGListSectionController相当于上面我们提到的ViewModel,你可以在里面控制列表中的单元如何显示

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

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

4、在IGListSectionController里面设置边距、Section里面有几个单元cell、每个cell又用的哪种类型,并用model填充cell

import IGListKit

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

// MARK: - IGListSectionType
extension MessageSectionController: IGListSectionType {
  func numberOfItems() -> Int {
    return 1
  }
  
  func sizeForItem(at index: Int) -> CGSize {
    guard let context = collectionContext else { return .zero }
    return MessageCell.cellSize(width: context.containerSize.width, text: message.text)
  }
  
  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
  }
  
  func didUpdate(to object: Any) {
    message = object as? Message
  }
  
  func didSelectItem(at index: Int) {}
}

5、当你的数据有更新时,你需要执行

adapter.performUpdates(animated: true)

但是看起来并没有Rxswift那么简单有没有,但是性能应该是没问题的。具体可以测试一下。

Written on April 3, 2018