- Add strictly the properties that you will use as opposed to all the properties delivered in the network response.
- Conform to
Codablewhen used to map network response - Use
CodingKeysenum 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"
}
}ViewModels implement protocols defined byviews. This protocols are specs of what a view needs from aviewModel.- 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) andObservers (to handle user interaction). Drivers should map the model into formatted data ready to be rendered in theview.
public var passengerName: Driver<String> {
return model
.map { $0.passenger }
.map { $0.name }
.asDriver(onErrorJustReturn: "")
}ViewModels hold abagfor disposing subscriptions- When creating a
viewModelfor aviewController, thisviewModelshould define a protocol for interaction with the corresponding Coordinator, aviewModeltipically delegates access of data and navigation events to theCoordinator. Observers are used to handle user interaction, and their naming should show this.Observers are backed by privately declaredPublishSubjects 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 exposeUIKitobjects or just logical values that allow for transformation in the views? e.g. should we exposecaptainStatuswhich the view maps to aUIColor, and anUIImagemaybe, or should we expose anUIImageinstance and aUIColorinstance?.
- Declaration of properties including a viewModel (when necessary) and a
disposableBagcalledbagfor 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 usingCartography, 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
viewneeds aviewModelwe 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 usinglazyfor every subview declaration which it turns preserves the meaning oflazyfor 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.
- Naming Subviews (same as with views).
- Building/Styling Subviews (same as with views).
-
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 {- 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
selfit's okay to use[unowned self]instead of[weak self] - Code inside subscriptions should be modeled in a Functional fashion (no side-effects).