Skip to content

Instantly share code, notes, and snippets.

@ivanbruel
Last active April 19, 2016 16:08
Show Gist options
  • Save ivanbruel/1227bf321161c46bb7a3ed804413ebce to your computer and use it in GitHub Desktop.
Save ivanbruel/1227bf321161c46bb7a3ed804413ebce to your computer and use it in GitHub Desktop.
DressDetailsAddToBagViewModel
//
// DressDetailsAddToBagViewModel.swift
// ChicByChoice
//
// Created by Ivan Bruel on 18/04/16.
// Copyright © 2016 Chic by Choice. All rights reserved.
//
import Foundation
import RxSwift
import RxCocoa
import Maya
import Device
class DressDetailsAddToBagViewModel: DressDetailsViewModel, ErrorAlertable {
private let _size: Variable<DressSize?>
private let _secondSize: Variable<DressSize?>
private let _cut: Variable<DressCut?>
private let _rentalPeriod: Variable<OrderPeriod>
private let _eventDate: Variable<MayaDate?>
private let _tryOnDate: Variable<MayaDate?>
private let _eventDateAvailabilities: Variable<[DressDetailsAvailabilityViewModel]>
private let _tryOnDateAvailabilities: Variable<[DressDetailsAvailabilityViewModel]>
var ctaViewModel: CTAAlertViewModel {
return CTAAlertViewModel(message: tr(.CTAMessageCall), image: UIImage(asset: .PhoneIcon),
buttonTitle: tr(.CTAButtonDismiss))
}
override func initViewModels(dress: Dress) -> [DressDetailsType] {
let size = DressDetailsAddToBag.Size(size: self.size, availableSizes: availableSizes,
selected: selectedSize)
let secondSize = DressDetailsAddToBag.SecondSize(secondSize: self.secondSize,
availableSecondSizes: availableSecondSizes,
secondSizeInformation: secondSizeInformation,
selected: selectedSecondSize)
let cut = DressDetailsAddToBag.Cut(cut: self.cut, availableCuts: availableCuts,
selected: selectedCut)
let rentalPeriod = DressDetailsAddToBag
.RentalPeriod(rentalPeriod: self.rentalPeriod, availableRentalPeriods: avaiableRentalPeriods,
selected: selectedRentalPeriod)
let deliveryDate = DressDetailsAddToBag
.DeliveryDate(deliveryDateText: deliveryDateText,
eventDateAvailability: eventDateAvailability,
eventDateAvailabilities: eventDateAvailabilities, maxDate: eventMaxDate,
load: loadEventDateAvailability,
selected: selectedEventDate)
let tryOnDate = DressDetailsAddToBag
.TryOnDate(tryOnDateText: tryOnDateText, tryOnDateAvailability: tryOnDateAvailability,
tryOnDateAvailabilities: tryOnAvailabilities, tryOnInformation: tryOnPricing,
maxDate: tryOnMaxDate, load: loadTryOnDateAvailability,
selected: selectedTryOnDate)
return [size, secondSize, cut, rentalPeriod, deliveryDate, tryOnDate].map { $0.viewModel }
}
// MARK: Size
var size: Observable<String?> {
return _size.asObservable()
.map { $0?.name(self.user.country) }
}
var availableSizes: Observable<[String]> {
return dress.asObservable().map { $0.sizes ?? [] }
.map { dressSizes in
dressSizes
.sort { $0.rawValue < $1.rawValue }
.map { $0.name(self.user.country) }
}
}
// MARK: Second Size
var secondSize: Observable<String?> {
return _secondSize.asObservable()
.map { $0?.name(self.user.country) }
}
var availableSecondSizes: Observable<[String]> {
let availableSizes: Observable<[DressSize]> = dress.asObservable().map({ $0.sizes ?? [] })
let size: Observable<DressSize?> = _size.asObservable()
return Observable.combineLatest(size, availableSizes) { ($0, $1) }
.map { (dressSize, availableSizes) in
guard let dressSize = dressSize,
secondDressSize = DressSize(rawValue: dressSize.rawValue + 1) else {
return []
}
return availableSizes.contains(secondDressSize) ?
[secondDressSize.name(self.user.country)] : []
}
}
var secondSizeInformation: Observable<String?> {
return Observable.combineLatest(size, availableSecondSizes, eventDate, eventDateAvailability) {
($0, $1, $2, $3)
}.map { (size, availableSecondSizes, eventDate, eventDateAvailability) in
guard size != nil else {
return tr(.DressDetailsLabelSelectSize)
}
guard eventDate != nil else {
return availableSecondSizes.count > 0 ? nil : tr(.DressDetailsLabelNotAvailable)
}
return eventDateAvailability != nil ? nil : tr(.DressDetailsLabelNotAvailable)
}
}
// MARK: Cut
var cut: Observable<String?> {
return _cut.asObservable().map { $0?.name }
}
var availableCuts: Observable<[String]> {
return Observable.combineLatest(_size.asObservable(), dress.asObservable()) { ($0, $1) }
.map { (size, dress) in
guard let cuts = dress.cutsForSize(size)
where cuts.count > 1 else {
return []
}
return cuts.map { $0.name }
}
}
// MARK: RentalPeriod
var rentalPeriod: Observable<String?> {
return _rentalPeriod.asObservable()
.map { $0.name }
}
var avaiableRentalPeriods: Observable<[String]> {
let orderPeriodNames = OrderPeriod.all().map { $0.name }
return Observable.just(orderPeriodNames)
}
// MARK: DeliveryDate
var eventDate: Observable<MayaDate?> {
return _eventDate.asObservable()
}
var deliveryDate: Observable<MayaDate?> {
return DressDetailsAddToBagViewModel
.deliveryDateObservable(_eventDate.asObservable(), availabilities: eventDateAvailabilities)
}
var deliveryDateText: Observable<String?> {
return deliveryDate.map { DressDetailsAddToBagViewModel.dateName($0) }
}
var eventMaxDate: Observable<MayaDate> {
return Observable.just(MayaMonth(mayaDate: MayaDate.today).monthWithOffset(11).lastDate)
}
var eventDateAvailabilities: Observable<[DressDetailsAvailabilityViewModel]> {
return _eventDateAvailabilities.asObservable()
.map { availabilities in
return availabilities.filter { $0.sizeAvailable }
}
}
var eventDateAvailability: Observable<DressDetailsAvailabilityViewModel?> {
return Observable.combineLatest(eventDateAvailabilities, eventDate) { ($0, $1) }
.map { (availabilities, eventDate) in
guard let eventDate = eventDate else {
return nil
}
return availabilities.filter { $0.date == eventDate }.first
}
}
// MARK: TryOnDate
var tryOnDate: Observable<MayaDate?> {
return DressDetailsAddToBagViewModel.deliveryDateObservable(_tryOnDate.asObservable(),
availabilities: tryOnAvailabilities)
}
var tryOnDateText: Observable<String?> {
return _tryOnDate.asObservable().map { DressDetailsAddToBagViewModel.dateName($0) }
}
var tryOnAvailabilities: Observable<[DressDetailsAvailabilityViewModel]> {
return _tryOnDateAvailabilities.asObservable()
.map { availabilities in
return availabilities.filter { $0.sizeAvailable }
}
}
var tryOnMaxDate: Observable<MayaDate> {
return eventDate.map { eventDate in
guard let eventDate = eventDate else {
return MayaMonth(mayaDate: MayaDate.today).monthWithOffset(11).lastDate
}
return eventDate
}
}
var tryOnDateAvailability: Observable<DressDetailsAvailabilityViewModel?> {
return Observable.combineLatest(tryOnAvailabilities, tryOnDate) { ($0, $1) }
.map { (availabilities, eventDate) in
guard let eventDate = eventDate else {
return nil
}
return availabilities.filter { $0.date == eventDate }.first
}
}
var tryOnPricing: Observable<String?> {
return Observable.just(user.tryOnServicePrice)
}
init(user: User, dress: Dress, failedToLoad: (() -> Void)? = nil,
oldGallery: DressPhotoGalleryViewModel? = nil) {
_size = Variable(DressDetailsAddToBagViewModel.sizeFromUser(user, forDress: dress))
_secondSize = Variable(nil)
_cut = Variable(nil)
_rentalPeriod = Variable(.FourDays)
_eventDate = Variable(nil)
_tryOnDate = Variable(nil)
_eventDateAvailabilities = Variable([])
_tryOnDateAvailabilities = Variable([])
super.init(user: user, confirmButtonTitle: tr(.DressDetailsButtonAddToBag), dress: dress,
failedToLoad: failedToLoad, oldGallery: oldGallery)
setupViewModels()
}
private func setupViewModels() {
setupSecondSizeObservable()
setupCutObservable()
setupEventDate()
}
private func setupSecondSizeObservable() {
let availableSizes: Observable<[DressSize]> = dress.asObservable().map({ $0.sizes ?? [] })
let size: Observable<DressSize?> = _size.asObservable()
.distinctUntilChanged { $0 == $1 }
Observable.combineLatest(size, availableSizes) { ($0, $1) }
.bindNext { (size, availableSizes) in
guard let size = size,
secondDressSize = DressSize(rawValue: size.rawValue + 1) else {
self._secondSize.value = nil
return
}
self._secondSize.value = availableSizes.contains(secondDressSize) ? secondDressSize : nil
}.addDisposableTo(rx_disposeBag)
}
private func setupCutObservable() {
_size.asObservable()
.distinctUntilChanged { $0 == $1 }
.bindNext { size in
guard let size = size else {
self._cut.value = nil
return
}
self._cut.value = self.dress.value.cutsForSize(size)?.first
}.addDisposableTo(rx_disposeBag)
}
private func setupEventDate() {
let size = _size.asObservable().distinctUntilChanged { $0 == $1 }
let secondSize = _secondSize.asObservable().distinctUntilChanged { $0 == $1 }
let rentalPeriod = _rentalPeriod.asObservable().distinctUntilChanged { $0 == $1 }
Observable.combineLatest(size, secondSize, rentalPeriod) { ($0, $1, $2) }
.bindNext { (size, secondSize, rentalPeriod) in
guard let size = size else {
return
}
self.loadAvailability(size, secondSize: secondSize, hirePeriod: rentalPeriod,
date: NSDate())
}.addDisposableTo(rx_disposeBag)
_eventDate.asObservable()
.distinctUntilChanged { $0 == $1 }
.bindNext { eventDate in
self._tryOnDate.value = nil
}.addDisposableTo(rx_disposeBag)
}
}
// MARK: Selection
extension DressDetailsAddToBagViewModel {
func selectedSize(size: String?) {
QL1("Selected size \(size)")
_size.value = DressSize.sizeFromString(size, country: user.country)
}
func selectedSecondSize(secondSize: String?) {
QL1("Selected second size \(secondSize)")
_secondSize.value = DressSize.sizeFromString(secondSize, country: user.country)
}
func selectedCut(cut: String?) {
QL1("Selected cut \(cut)")
_cut.value = DressCut.cutFromString(cut)
}
func selectedRentalPeriod(rentalPeriod: String?) {
QL1("Selected rental period \(rentalPeriod)")
_rentalPeriod.value = OrderPeriod.orderPeriodFromString(rentalPeriod) ?? .FourDays
}
func selectedEventDate(eventDate: MayaDate?) -> Bool {
QL1("Selected event date \(eventDate?.date)")
_eventDate.value = eventDate
return true
}
func selectedTryOnDate(tryOnDate: MayaDate?) -> Bool {
QL1("Selected try on date \(tryOnDate?.date)")
_tryOnDate.value = tryOnDate
return true
}
}
// MARK: Availability
extension DressDetailsAddToBagViewModel {
func loadEventDateAvailability(date: MayaDate) {
guard let size = _size.value else {
return
}
loadAvailability(size, secondSize: _secondSize.value, hirePeriod: _rentalPeriod.value,
date: date.date)
}
func loadTryOnDateAvailability(date: MayaDate) {
guard let size = _size.value else {
return
}
loadAvailability(size, secondSize: _secondSize.value, hirePeriod: .TwoDays,
date: date.date)
}
}
// MARK: Networking
extension DressDetailsAddToBagViewModel {
func addToBag() -> Observable<Order> {
self.isPerformingARequest.value = true
guard let dressSize = _size.value else {
self.isPerformingARequest.value = false
return .error(AddToBagError.EmptyFields(tr(.DressDetailsErrorSizeNotPicked)))
}
let freeSecondSize = _secondSize.value
let cut: DressCut? = _cut.value
let rentalPeriod: OrderPeriod = _rentalPeriod.value
guard let deliveryDate = DressDetailsAddToBagViewModel
.deliveryDate(_eventDate.value, availabilities: _eventDateAvailabilities.value)?.date else {
self.isPerformingARequest.value = false
return .error(AddToBagError.EmptyFields(tr(.DressDetailsErrorDeliveryDateNotPicked)))
}
guard let returnDate = DressDetailsAddToBagViewModel
.returnDate(_eventDate.value, availabilities: _eventDateAvailabilities.value)?.date else {
self.isPerformingARequest.value = false
return .error(AddToBagError.FetchingReturnDate)
}
let tryOnService = _tryOnDate.value?.date
return Network.provider.request(API.ShoppingBagAdd(dress.value, dressSize, freeSecondSize, cut,
rentalPeriod, deliveryDate, returnDate, tryOnService))
.filterSuccessfulStatusCodes()
.observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.mapObject(Order)
.observeOn(MainScheduler.instance)
.doOn { _ in
self.isPerformingARequest.value = false
}.doOnNext { order in
self.user.shoppingBag = order
self.user.saveEventually()
}
}
func loadAvailability(size: DressSize, secondSize: DressSize?, hirePeriod: OrderPeriod,
date: NSDate) {
guard let size = _size.value else {
return
}
let freeSecondSize = _secondSize.value
QL1("Loading availability \(size.rawValue) \(freeSecondSize?.rawValue) \(date) \(hirePeriod.rawValue)")
Network.provider.request(API.DressAvailability(self.dress.value.identifier,
size.rawValue, freeSecondSize?.rawValue, date, hirePeriod.rawValue))
.filterSuccessfulStatusCodes()
.observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.mapArray(DressAvailability)
.observeOn(MainScheduler.instance)
.bindNext { availabilityArray in
let availabilityViewModelArray = availabilityArray
.filter { $0.deliveryDate != nil }
.filter { MayaDate(date: $0.date).compare(MayaDate.today) == .OrderedDescending }
.map { DressDetailsAvailabilityViewModel(dressAvailability: $0) }
if hirePeriod == .TwoDays {
self._tryOnDateAvailabilities.value += availabilityViewModelArray
} else {
self._eventDateAvailabilities.value += availabilityViewModelArray
}
}.addDisposableTo(rx_disposeBag)
}
}
// MARK: Helpers
extension DressDetailsAddToBagViewModel {
private class func deliveryDateObservable(
date: Observable<MayaDate?>,
availabilities: Observable<[DressDetailsAvailabilityViewModel]>) -> Observable<MayaDate?> {
return Observable.combineLatest(date, availabilities) { ($0, $1) }
.map { (date, availabilities) in
return deliveryDate(date, availabilities: availabilities)
}
}
private class func deliveryDate(
date: MayaDate?,
availabilities: [DressDetailsAvailabilityViewModel]) -> MayaDate? {
guard let availability = availabilities.filter({ $0.date == date }).first,
deliveryDate = availability.deliveryDate else {
return nil
}
return deliveryDate
}
private class func returnDate(date: MayaDate?,
availabilities: [DressDetailsAvailabilityViewModel]) -> MayaDate? {
guard let availability = availabilities.filter({ $0.date == date }).first else {
return nil
}
return availability.returnDate
}
private class func sizeFromUser(user: User, forDress dress: Dress) -> DressSize? {
guard let sizes = dress.sizes else { return nil }
if let size = user.session.filters.value.sizes?.first where sizes.contains(size) {
return size
}
if let size = user.size where sizes.contains(size) {
return size
}
return nil
}
private class func dateName(date: MayaDate?) -> String? {
return Device.isSmallerThanScreenSize(.Screen4_7Inch) ? date?.date.dateValue :
date?.date.localizedLongValue
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment