Skip to content

Instantly share code, notes, and snippets.

@andrespch
Last active July 3, 2018 12:30
Show Gist options
  • Save andrespch/b2f5cb1b0a32610acea5c327ce566448 to your computer and use it in GitHub Desktop.
Save andrespch/b2f5cb1b0a32610acea5c327ce566448 to your computer and use it in GitHub Desktop.

Model

  • Add strictly the properties that you will use as opposed to all the properties delivered in the network response.
  • Conform to Codable when used to map network response
  • Use CodingKeys enum when mapping a reduced part of the response or when needing to specify different names (specially if the names coming from the server make little sense).
  • Question: Class or Struct Convention?
struct SelectCarResponse: Codable {
    let carDriverId: Int
    let id: Int
    let manualTripAllowed: Bool
    let walkInTripAllowed: Bool
    let car: Car
    let captainCarType: CaptainCarType
    
    private enum CodingKeys: String, CodingKey {
        case carDriverId
        case id = "carId"
    }
}

ViewModel

  • ViewModels implement protocols defined by views. This protocols are specs of what a view needs from a viewModel.
  • File structure should have the following order: declaration of properties, init method (where we can configure the subscriptions), functions to handle actions privately defined, comformance to protocols.
  • This protocol typically defines Rx Drivers (to drive UI elements) and Observers (to handle user interaction).
  • Drivers should map the model into formatted data ready to be rendered in the view.
public var passengerName: Driver<String> {
    return model
        .map { $0.passenger }
        .map { $0.name }
        .asDriver(onErrorJustReturn: "")
}
  • ViewModels hold a bag for disposing subscriptions
  • When creating a viewModel for a viewController, this viewModel should define a protocol for interaction with the corresponding Coordinator, a viewModel tipically delegates access of data and navigation events to the Coordinator .
  • Observers are used to handle user interaction, and their naming should show this.
  • Observers are backed by privately declared PublishSubjects whose name are the same as their observer counter part, presceded with an underscore.
    private let _callPassenger = PublishSubject<Void>()

    // use the publicSubject as Observer
    public var callPassengerTapped: AnyObserver<Void> {
        return _callPassenger.asObserver()
    }
    // use the publicSubject as Observable
    _callPassenger
        .asObservable()
        .mapTo(booking.passenger.phoneNumber)
        .subscribe(onNext: { [unowned self] phoneNumber in
            self.callPassenger(phoneNumber: phoneNumber)
        })
        .disposed(by: bag)
  • As seen on the example above, when needed we can create functions to hold the business logic instead of executing it directly from the subscription closure, this also allows for separation of validation code from the actual final execution of the action that corresponds to the name of the method (callPassengerTapped vs callPassenger)
  • To allow for easy readability we can separate observers from drivers using //MARK:
  • Question: Should viewModels expose UIKit objects or just logical values that allow for transformation in the views? e.g. should we expose captainStatus which the view maps to a UIColor, and an UIImage maybe, or should we expose an UIImage instance and a UIColor instance?.

View

  • Declaration of properties including a viewModel (when necessary) and a disposableBag called bag for conciseness.
  • Init method should be injected a viewModel (when necessary), it should call super and a have private methods to configure layout and bind view model.
public init () {
    super.init(frame: .zero)
    layoutSubviews()
    bind(viewModel: ) // <-- only when needed
}
  • Include an expressive message when erroring in the required init.
required public init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) is not allowed for CallCustomerView")
}
  • In layoutSubviews() we configure layout using Cartography, when passing the subviews involved in the layout we can use a different naming inside the closure so that its easier to understand how a view relates to the other in terms of UI.
addSubview(ratingView)
constrain(self, ratingView, nameLabel) { (superview, view, topView) in
    view.height == 16
    view.leading == superview.leading + 16
    view.top == topView.bottom + 50
}
  • Whenever a view needs a viewModel we define a function that takes care of binding and returning all disposables together, this means that views don't need to create a bag and handle disposables.
func bind(viewModel:) -> Disposable
  • Declaration of subviews should be at the top and one-liners declared as let. Customization of this subviews should take place separately favoring clarity at the top of the file. With this strategy we avoid using lazy for every subview declaration which it turns preserves the meaning of lazy for other use cases.
  • Text styles or other Style definitions shuold be placed at the bottom of the file after the customization and layout methods
  • When interacting with a viewModel, a view should define a protocol as a contract for this interaction.

ViewController

  • Naming Subviews (same as with views).
  • Building/Styling Subviews (same as with views).

Protocol Conformance

  • Protocol conformance shuold be place in a separate extension as long as the class contains any code other than this protocol implememtation.

  • Protocol conformance declaration should be added at class/struct declaration to provide the reader with all relevant information without needing to navigate the whole file.

    public class JobInformationViewModel: JobInformationViewModelType {
  • //MARK: - delimiters should be add to allow for easy navigation.
// MARK: - JobInformationViewModelType
extension JobInformationViewModel {

Rx Subscriptions

  • Every operator should go in a separate line.
 inRideViewModels
            .flatMapLatest { $0.rideEnded.completions }
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: {[unowned self] in
                self.delegate?.dismissInRide()
                self.delegate?.rideEnded()
            })
            .disposed(by: bag)
  • Code inside subscriptions shuold stay lean.
  • No subscriptions should take place inside other subscriptions.
  • If the subscription is coming from a property of self it's okay to use [unowned self] instead of [weak self]
  • Code inside subscriptions should be modeled in a Functional fashion (no side-effects).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment