- 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
s implement protocols defined byview
s. 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
Driver
s (to drive UI elements) andObserver
s (to handle user interaction). Driver
s 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: "")
}
ViewModel
s hold abag
for disposing subscriptions- When creating a
viewModel
for aviewController
, thisviewModel
should define a protocol for interaction with the corresponding Coordinator, aviewModel
tipically delegates access of data and navigation events to theCoordinator
. Observer
s are used to handle user interaction, and their naming should show this.Observer
s are backed by privately declaredPublishSubject
s 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
viewModel
s exposeUIKit
objects or just logical values that allow for transformation in the views? e.g. should we exposecaptainStatus
which the view maps to aUIColor
, and anUIImage
maybe, or should we expose anUIImage
instance and aUIColor
instance?.
- Declaration of properties including a viewModel (when necessary) and a
disposableBag
calledbag
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 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
view
needs aviewModel
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 usinglazy
for every subview declaration which it turns preserves the meaning oflazy
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.
- 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
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).