fsaar/SASeatBooking

View on GitHub
SASeatBooking/SASeatBookingView/SAViewController.swift

Summary

Maintainability
A
25 mins
Test Coverage
//
//  ViewController.swift
//  SASeatBooking
//
//  Created by Frank Saar on 08/08/2017.
//  Copyright © 2017 SAMedialabs. All rights reserved.
//

import UIKit
import SceneKit

fileprivate let seatMap : [[SASeatFactoryType]]  = [
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.occupied,.space,.occupied,.occupied,.occupied,.occupied,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.available,.available,.available,.space,.available,.available,.available,.available,.available,.space,.occupied,.occupied,.occupied],
    [.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,.space,]
]

class SAViewController: UIViewController {
    
    fileprivate var defaultSeatSize = (width: CGFloat(1),height:CGFloat(1),length:CGFloat(1))
    fileprivate lazy var seatFactory = SASeatFactory()
    fileprivate var badgeAction : SCNAction = {
        let action1 = SCNAction.move(by: SCNVector3Make(0, 0.5, 0.5), duration: 0.25)
        let action2 = SCNAction.move(by: SCNVector3Make(0, 1, -1), duration: 0.5)
        let action = SCNAction.sequence([action1,action2])
        return action
    }()
    @IBOutlet var sceneView : SASeatBookingView! = nil {
        didSet {
            self.sceneView.seatDataSource = self
            self.sceneView.seatDelegate = self
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        sceneView = SASeatBookingView(frame: self.view.bounds, options: nil,with: true)
        sceneView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(sceneView)
        NSLayoutConstraint.activate([
            self.view.leadingAnchor.constraint(equalTo: sceneView.leadingAnchor),
            self.view.trailingAnchor.constraint(equalTo: sceneView.trailingAnchor),
            self.view.bottomAnchor.constraint(equalTo: sceneView.bottomAnchor),
            self.view.topAnchor.constraint(equalTo: sceneView.topAnchor,constant: 20),
        ])
        sceneView.startAnimation()
    }
}

extension SAViewController : SASeatBookingViewDatasource {
    func numberOfRowsAndColumnsInSeatBookingView(_ view : SASeatBookingView) -> SASeatBookingSize {
        return (seatMap.count,seatMap.first?.count ?? 0)
    }
    
    func seatBookingView(_ view: SASeatBookingView,nodeAt position:  SASeatPosition) -> SCNNode? {
        let row = seatMap[position.row]
        let type = row[position.column]
        let columnOffset = row[0..<position.column].filter{ $0 == .space }.count
        let labelPosition = (column: position.column - columnOffset,row: position.row)
        let seatLabel = self.label(for: labelPosition)
        guard type != .space, let seat = self.seatFactory.seatNode(of: type,with: seatLabel) else {
            return nil
        }
        return seat
    }
}

extension SAViewController : SASeatBookingViewDelegate {
    func seatBookingView(_ view: SASeatBookingView,canSelectSeatAt position: SASeatPosition)  -> Bool{
        let type = seatMap[position.row][position.column]
        return type == .available
    }
    
    func seatBookingView(_ view: SASeatBookingView,didSelect seat : SCNNode,  at position: SASeatPosition) {
        addBodyIfNeedBe(to: seat)
        animated(show: true, bodyOf: seat)
    }
    func seatBookingView(_ view: SASeatBookingView,didDeselect seat : SCNNode, at position: SASeatPosition) {
        animated(show: false, bodyOf: seat)
    }
}

//
// MARK: - Helper
//
fileprivate extension SAViewController {
    func addBodyIfNeedBe(to node: SCNNode) {
        if case .none =  node.childNode(withName: SASeatFactoryLabel.body.rawValue, recursively: true) {
            if let body = self.seatFactory.body {
                body.opacity = 0
                node.addChildNode(body)
            }
        }
    }
    
    func animated(show : Bool,bodyOf node: SCNNode) {
        if let body =  node.childNode(withName: SASeatFactoryLabel.body.rawValue, recursively: true) {
            let action = show ? SCNAction.fadeOpacity(to: 1, duration: 1) : SCNAction.fadeOpacity(to: 0, duration: 1)
            body.runAction(action)
        }
        if let badge =  node.childNode(withName: SASeatFactoryLabel.badge.rawValue, recursively: true) {
            let action = show ? self.badgeAction : self.badgeAction.reversed()
            badge.runAction(action)
        }
    }
    
    func label(for position : SASeatPosition) -> String {
        guard let letter = UnicodeScalar(65+position.column) else {
            return ""
        }
        let label = "\(position.row+1)\(letter)"
        return label
    }
}