Skip to content

Instantly share code, notes, and snippets.

@TosinAF
Last active January 20, 2021 14:06
Show Gist options
  • Save TosinAF/3c5859d07d120ffa3a44 to your computer and use it in GitHub Desktop.
Save TosinAF/3c5859d07d120ffa3a44 to your computer and use it in GitHub Desktop.
Example use of the Model-View-ViewModel Pattern in iOS (Swift) as explained in http://www.objc.io/issue-13/mvvm.html. Full Source Code can be found at https://github.com/TosinAF/HopperBus-iOS

I decided to build an iOS app for my University's bus service that runs through the various campuses.

xy

It was an interesting challenge as I had nothing but the printed timetables (http://www.nottingham.ac.uk/about/documents/903-times.pdf) to use as the data.

Thus I had to come with a suitable data structure that would complement the design & user experience i had in mind for the app.

I also decided to take the challenge of writing the app in swift. This project has helped me get to up speed with swift really quickly.

I wanted to follow best practises so I decided to use the Model-View-ViewModel Pattern which avoids the problem of having a massive view controller which reduces it's reusability & creates much messier code.

The resulting code can be seen below & I actually loved using this pattern as it really helps with the separation of concerns of objects.

//
// Models.swift
// HopperBus
//
// Created by Tosin Afolabi on 26/09/2014.
// Copyright (c) 2014 Tosin Afolabi. All rights reserved.
//
import Foundation
struct Route {
let termTime: [Schedule]
var saturdays: [Schedule]?
var holidays: [Schedule]?
var schedules: [Schedule] {
if saturdays == nil || holidays == nil {
return termTime
}
if let sat = saturdays {
if NSDate.isSaturday() { return sat }
}
if let hol = holidays {
if NSDate.isHoliday() { return hol }
}
return termTime
}
init(termTime: [Schedule]) {
self.termTime = termTime
}
}
struct Schedule {
let stops: [Stop]
}
struct Stop {
let id: String
let name: String
let time: String
}
struct Times {
let stopID: String
let stopName: String
var termTime: [String]
var saturdays: [String]?
var holidays: [String]?
var currentTimes : [String] {
if saturdays == nil || holidays == nil {
return termTime
}
if let sat = saturdays {
if NSDate.isSaturday() { return sat }
}
if let hol = holidays {
if NSDate.isHoliday() { return hol }
}
return termTime
}
init(stopID: String, name: String, termTime: [String]) {
self.stopID = stopID
self.stopName = name
self.termTime = termTime
}
}
struct APIRoute {
let stops: [APIStop]
}
struct APIStop {
let name: String
let code: String
let coord: CLLocationCoordinate2D
}
//
// RouteViewController.swift
// HopperBus
//
// Created by Tosin Afolabi on 22/08/2014.
// Copyright (c) 2014 Tosin Afolabi. All rights reserved.
//
import UIKit
class RouteViewController: UIViewController {
// MARK: - Properties
let routeType: HopperBusRoutes!
let routeViewModel: RouteViewModel!
var animTranslationStart: CGPoint!
var animTranslationFinish: CGPoint!
lazy var routeHeaderView: RouteHeaderView = {
let view = RouteHeaderView()
view.titleLabel.text = self.routeType.title.uppercaseString
return view
}()
lazy var tableView: TableView = {
let tableView = TableView()
tableView.delegate = self
tableView.dataSource = self
tableView.doubleTapDelegate = self
tableView.separatorStyle = .None
tableView.contentInset = UIEdgeInsetsMake(64.0, 0.0, 64.0, 0.0);
tableView.registerClass(StopTableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
lazy var animatedCircleView: UIView = {
let circleView = UIView(frame: CGRectMake(0, 0, 14, 14))
circleView.backgroundColor = UIColor.selectedGreen()
circleView.layer.borderColor = UIColor.selectedGreen().CGColor
circleView.layer.cornerRadius = 7
return circleView
}()
// MARK: - Initalizers
init(type: HopperBusRoutes, routeViewModel: RouteViewModel) {
self.routeType = type
self.routeViewModel = routeViewModel
super.init(nibName: nil, bundle: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.frame
tableView.rowHeight = UITableViewAutomaticDimension;
tableView.estimatedRowHeight = self.routeType == HopperBusRoutes.HB904 ? 65 : 55
}
}
// MARK: - TableViewDataSource & Delegate Methods
extension RouteViewController: UITableViewDelegate, UITableViewDataSource, TableViewDoubleTapDelegate {
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return routeHeaderView
}
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 70
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return routeViewModel.numberOfStopsForCurrentRoute()
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return self.routeType == HopperBusRoutes.HB904 ? 65 : 55
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as StopTableViewCell
let rvm = self.routeViewModel
let index = indexPath.row
cell.titleLabel.text = rvm.nameForStop(index)
cell.timeLabel.text = index == rvm.stopIndex ? rvm.timeTillStop(index) : rvm.timeForStop(index)
cell.isLastCell = index == rvm.numberOfStopsForCurrentRoute() - 1 ? true : false
cell.isSelected = index == rvm.stopIndex ? true : false
cell.height = self.routeType == HopperBusRoutes.HB904 ? 65 : 55
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedIndex = indexPath.row
if selectedIndex == self.routeViewModel.stopIndex { return }
let animPoints = self.getAnimationTranslationPoints(tableView, selectedIndex: selectedIndex)
(self.animTranslationStart, self.animTranslationFinish) = animPoints
self.animateSelection()
let oldIndexPath = NSIndexPath(forRow: self.routeViewModel.stopIndex , inSection: 0)
if let oldCell = tableView.cellForRowAtIndexPath(oldIndexPath) as? StopTableViewCell {
self.animatedCircleView.center = self.animTranslationStart
self.view.addSubview(self.animatedCircleView)
oldCell.isSelected = false
let timeStr = self.routeViewModel.timeForStop(oldIndexPath.row)
oldCell.animateTimeLabelTextChange(timeStr)
}
self.routeViewModel.stopIndex = selectedIndex
}
func tableView(tableView: TableView, didDoubleTapRowAtIndexPath indexPath: NSIndexPath) {
let rvm = self.routeViewModel
let times = rvm.stopTimingsForStop(rvm.idForStop(indexPath.row))
let timesViewController = TimesViewController()
timesViewController.times = times
timesViewController.modalPresentationStyle = .Custom
timesViewController.transitioningDelegate = self
presentViewController(timesViewController, animated: true, completion:nil)
}
}
// MARK: - POPAnimation Delegate
extension RouteViewController: POPAnimationDelegate {
// MARK:- Animatons
func animateSelection() {
let anim = createScaleAnimation("scaleDown", from: 1.0, to: 0.5)
animatedCircleView.layer.pop_addAnimation(anim, forKey: "scaleDown")
view.userInteractionEnabled = false
}
func pop_animationDidStop(anim: POPAnimation!, finished: Bool) {
if anim.name == "scaleDown" {
let anim = createYTranslationAnimation("yTranslate", from: animTranslationStart, to: animTranslationFinish)
animatedCircleView.pop_addAnimation(anim, forKey: "center")
} else if anim.name == "yTranslate" {
let anim = createScaleAnimation("scaleUp", from: 0.5, to: 1.0)
animatedCircleView.layer.pop_addAnimation(anim, forKey: "scaleUp")
} else {
let indexPath = NSIndexPath(forRow: routeViewModel.stopIndex , inSection: 0)
let cell = tableView.cellForRowAtIndexPath(indexPath) as StopTableViewCell
let timeStr = routeViewModel.timeTillStop(indexPath.row)
cell.isSelected = true
cell.animateTimeLabelTextChange(timeStr)
animatedCircleView.removeFromSuperview()
tableView.reloadData()
view.userInteractionEnabled = true
}
}
func createScaleAnimation(name: String, from start: CGFloat, to finish: CGFloat) -> POPSpringAnimation {
let routeScaleXYAnim = POPSpringAnimation(propertyNamed: kPOPLayerScaleXY)
routeScaleXYAnim.name = name
routeScaleXYAnim.delegate = self
routeScaleXYAnim.springBounciness = 5
routeScaleXYAnim.springSpeed = 18
routeScaleXYAnim.fromValue = NSValue(CGSize: CGSizeMake(start, start))
routeScaleXYAnim.toValue = NSValue(CGSize: CGSizeMake(finish, finish))
return routeScaleXYAnim
}
func createYTranslationAnimation(name: String, from start: CGPoint, to final: CGPoint) -> POPBasicAnimation {
let yTranslationAnimation = POPBasicAnimation(propertyNamed: kPOPViewCenter)
yTranslationAnimation.name = name
yTranslationAnimation.delegate = self
yTranslationAnimation.fromValue = NSValue(CGPoint: start)
yTranslationAnimation.toValue = NSValue(CGPoint: final)
return yTranslationAnimation
}
func getAnimationTranslationPoints(tableView: UITableView, selectedIndex: Int) -> (CGPoint!, CGPoint!) {
let oldIndexPath = NSIndexPath(forRow: self.routeViewModel.stopIndex , inSection: 0)
var start, finish: CGPoint
if let oldCell = tableView.cellForRowAtIndexPath(oldIndexPath) as? StopTableViewCell {
start = oldCell.convertPoint(oldCell.circleView.center, toView: view)
} else {
var cell: StopTableViewCell
if selectedIndex > routeViewModel.stopIndex {
// Animation Going Down
let indexPath = tableView.indexPathsForVisibleRows()![1] as NSIndexPath
cell = tableView.cellForRowAtIndexPath(indexPath)! as StopTableViewCell
} else {
// Animation Going Up On A Tuesday lol
let visibleCellCount = tableView.indexPathsForVisibleRows()!.count
let indexPath = tableView.indexPathsForVisibleRows()![visibleCellCount - 1] as NSIndexPath
cell = tableView.cellForRowAtIndexPath(indexPath)! as StopTableViewCell
}
start = cell.convertPoint(cell.circleView.center, toView: self.view)
}
let newIndexPath = NSIndexPath(forRow: selectedIndex, inSection: 0)
let newCell = tableView.cellForRowAtIndexPath(newIndexPath) as StopTableViewCell
finish = newCell.convertPoint(newCell.circleView.center, toView: self.view)
return (start, finish)
}
}
// MARK: - Transitioning Delegate
extension RouteViewController: UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PresentTimesTransistionManager()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissTimesTransistionManager()
}
}
//
// RouteViewModel.swift
// HopperBus
//
// Created by Tosin Afolabi on 24/09/2014.
// Copyright (c) 2014 Tosin Afolabi. All rights reserved.
//
// MARK: - HopperBusRoutes Enum
enum HopperBusRoutes: Int {
case HB901 = 0, HB902, HBRealTime, HB903, HB904
var title: String {
let routeTitles = [
"901 - Sutton Bonington",
"902 - King's Meadow",
"REAL TIME",
"903 - Jubilee Campus",
"904 - Royal Derby Hospital"
]
return routeTitles[rawValue]
}
var routeCode: String {
let routeCodes = [
"901",
"902",
"RT",
"903",
"904"
]
return routeCodes[rawValue]
}
static let allCases: [HopperBusRoutes] = [.HB902, .HB902, .HBRealTime, .HB903, .HB904]
}
// MARK: - RouteViewModelContainer Class
class RouteViewModelContainer {
let routeViewModels: [String: RouteViewModel]
init() {
let filePath = NSBundle.mainBundle().pathForResource("Routes", ofType: "json")!
let data = NSData(contentsOfFile: filePath, options: nil, error: nil)!
let json = JSON(data: data)
let data902 = json["route902"].dictionaryValue
let data903 = json["route903"].dictionaryValue
let data904 = json["route904"].dictionaryValue
let route902 = RouteViewModel(data: data902, type: .HB902)
let route903 = RouteViewModel(data: data903, type: .HB903)
let route904 = RouteViewModel(data: data904, type: .HB904)
routeViewModels = [
HopperBusRoutes.HB902.routeCode: route902,
HopperBusRoutes.HB903.routeCode: route903,
HopperBusRoutes.HB904.routeCode: route904
]
}
func routeViewModel(type: HopperBusRoutes) -> RouteViewModel {
return routeViewModels[type.routeCode]!
}
func updateScheduleIndexForRoutes() {
for (key,routeVM) in routeViewModels {
routeVM.updateScheduleIndex()
}
}
}
// MARK: - RouteViewModel Class
class RouteViewModel {
// MARK: - Properties
let route: Route
let routeType: HopperBusRoutes
let stopTimings: [String: Times]
var stopIndex: Int = 0 {
didSet { updateScheduleIndex() }
}
var scheduleIndex: Int = 0
// MARK: - Public Methods
init(data: [String: JSON], type: HopperBusRoutes) {
self.route = RouteViewModel.getRoute(data)
self.routeType = type
self.stopTimings = RouteViewModel.getStopTimings(data)
self.scheduleIndex = RouteViewModel.getScheduleIndexForCurrentTime(inRoute: route, atStop: stopIndex)
}
func updateScheduleIndex() {
self.scheduleIndex = RouteViewModel.getScheduleIndexForCurrentTime(inRoute: route, atStop: stopIndex)
}
func numberOfStopsForCurrentRoute() -> Int {
return route.schedules[scheduleIndex].stops.count
}
func nameForStop(index: Int) -> String {
let stop = route.schedules[scheduleIndex].stops[index]
return stop.name
}
func idForStop(index: Int) -> String {
let stop = route.schedules[scheduleIndex].stops[index]
return stop.id
}
func timeForStop(index: Int) -> String {
let stop = route.schedules[scheduleIndex].stops[index]
let formattedTime = formatTimeStringForDisplay(stop.time)
return formattedTime
}
func timeTillStop(index: Int) -> String {
let stop = route.schedules[scheduleIndex].stops[index]
let currentTimeStr = NSDate.currentTimeAsString()
let stopTimeStr = stop.time
let dateFormatter = NSDateFormatter()
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
dateFormatter.dateFormat = "HH:mm"
let currentTime = dateFormatter.dateFromString(currentTimeStr)!
let stopTime = dateFormatter.dateFromString(stopTimeStr)!
let diff = stopTime.timeIntervalSinceDate(currentTime)
switch abs(diff) {
case 0...60:
return "1m"
case 61..<3600:
let minutes = Int(floor(abs(diff) / 60))
return "\(minutes)m"
default:
let formattedTime = formatTimeStringForDisplay(stop.time)
return formattedTime
}
}
func stopTimingsForStop(id: String) -> Times {
return stopTimings[id]!
}
}
// MARK: - Private Methods
private extension RouteViewModel {
class func getRoute(data: [String: JSON]) -> Route {
let termTime = data["term_time_schedule"]!.arrayValue
let termTimeSchedules = RouteViewModel.getSchedules(termTime)
var route = Route(termTime: termTimeSchedules)
if let saturdays = data["saturday_schedule"] {
route.saturdays = RouteViewModel.getSchedules(saturdays.arrayValue)
}
if let holidays = data["holiday_schdule"] {
route.holidays = RouteViewModel.getSchedules(holidays.arrayValue)
}
return route
}
class func getSchedules(route: [JSON]) -> [Schedule] {
var schedules = [Schedule]()
for schedule in route {
var stops = [Stop]()
for stop in schedule.arrayValue {
let id = stop[0].stringValue
let name = stop[1].stringValue
let time = stop[2].stringValue
let s = Stop(id: id, name: name, time: time)
stops.append(s)
}
schedules.append(Schedule(stops: stops))
}
return schedules
}
class func getStopTimings(data: [String: JSON]) -> [String: Times] {
var stopTimings = [String: Times]()
let stopTimingsJSON = data["stop_times"]!.arrayValue
for stop in stopTimingsJSON {
let stopID = stop["id"].stringValue
let stopName = stop["name"].stringValue
let termTimeStopTimes = stop["term_time"].arrayValue
let termTime = RouteViewModel.castToStringArray(termTimeStopTimes)
var timings = Times(stopID: stopID, name: stopName, termTime: termTime)
if let saturdayTimes = stop["saturdays"].array {
timings.saturdays = RouteViewModel.castToStringArray(saturdayTimes)
}
if let holidayTimes = stop["holidays"].array {
timings.holidays = RouteViewModel.castToStringArray(holidayTimes)
}
stopTimings[stopID] = timings
}
return stopTimings
}
class func getScheduleIndexForCurrentTime(inRoute route: Route, atStop stopIndex: Int) -> Int {
let dateFormatter = NSDateFormatter()
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
dateFormatter.dateFormat = "HH:mm"
let currentTime = dateFormatter.dateFromString(NSDate.currentTimeAsString())
for (index, schedule) in enumerate(route.schedules) {
let time = schedule.stops[stopIndex].time
let possibleRouteTimeStart = dateFormatter.dateFromString(schedule.stops[stopIndex].time)
let result = currentTime!.compare(possibleRouteTimeStart!)
switch (result) {
case .OrderedAscending, .OrderedSame:
if index == 0 { return 1 }
return index
case .OrderedDescending:
continue
}
}
return 0
}
class func castToStringArray(jsonArr: [JSON]) -> [String] {
var strArray = [String]()
for element in jsonArr {
strArray.append(element.stringValue)
}
return strArray
}
func formatTimeStringForDisplay(timeStr: String) -> String {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "HH:mm"
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
let timeAsDate = dateFormatter.dateFromString(timeStr)
dateFormatter.dateFormat = "H:mm a"
return dateFormatter.stringFromDate(timeAsDate!)
}
}
private extension NSDate {
class func currentTimeAsString() -> String {
let dateFormatter = NSDateFormatter()
dateFormatter.timeStyle = .ShortStyle
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
dateFormatter.dateFormat = "HH:mm"
return dateFormatter.stringFromDate(NSDate())
}
}
//
// StopTableViewCell.swift
// HopperBus
//
// Created by Tosin Afolabi on 22/08/2014.
// Copyright (c) 2014 Tosin Afolabi. All rights reserved.
//
import UIKit
import QuartzCore
class StopTableViewCell: UITableViewCell {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.setTranslatesAutoresizingMaskIntoConstraints(false)
label.font = UIFont(name: "Avenir", size: 14)
label.numberOfLines = 2
return label
}()
lazy var timeLabel: UILabel = {
let label = UILabel()
label.setTranslatesAutoresizingMaskIntoConstraints(false)
label.font = UIFont(name: "Avenir-Light", size: 14)
label.textColor = UIColor(red: 0.631, green: 0.651, blue: 0.678, alpha: 1)
label.textAlignment = .Right
return label
}()
lazy var lineView: UIView = {
let view = UIView()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
view.backgroundColor = UIColor(red: 0.906, green: 0.914, blue: 0.918, alpha: 1)
return view
}()
lazy var circleView: UIView = {
let view = UIView()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
view.backgroundColor = UIColor.whiteColor()
view.layer.borderWidth = 1
view.layer.cornerRadius = 7
view.layer.borderColor = UIColor.disabledGreen().CGColor
return view
}()
var isLastCell: Bool = false {
willSet {
let constant: CGFloat = newValue ? -0.5 * self.contentView.frame.size.height : 0
lineViewYConstraint.constant = constant
}
}
var isSelected: Bool = false {
willSet(selected) {
if selected {
circleView.backgroundColor = UIColor.selectedGreen()
circleView.layer.borderColor = UIColor.selectedGreen().CGColor
timeLabel.textColor = UIColor.selectedGreen()
} else {
circleView.backgroundColor = UIColor.whiteColor()
circleView.layer.borderColor = UIColor.disabledGreen().CGColor
timeLabel.textColor = UIColor(red: 0.631, green: 0.651, blue: 0.678, alpha: 1)
}
}
}
var height: CGFloat = 55 {
willSet(height) {
contentView.removeConstraint(heightConstraint)
heightConstraint.constant = height
contentView.addConstraint(heightConstraint)
}
}
lazy var lineViewYConstraint: NSLayoutConstraint = {
return NSLayoutConstraint(item: self.lineView, attribute: .Height, relatedBy: .Equal, toItem: self.contentView, attribute: .Height, multiplier: 1.0, constant: 0)
}()
lazy var heightConstraint: NSLayoutConstraint = {
return NSLayoutConstraint(item: self.contentView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 55)
}()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .None
contentView.addSubview(titleLabel)
contentView.addSubview(timeLabel)
contentView.addSubview(lineView)
contentView.addSubview(circleView)
let views = [
"timeLabel": timeLabel,
"titleLabel": titleLabel,
"lineView": lineView,
"circleView": circleView,
"contentView": contentView
]
let metrics = [
"margin": 6,
"leftMargin": 16,
"lineMargin": 14
]
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-(leftMargin)-[timeLabel(80)]-(lineMargin)-[lineView(2)]-(lineMargin)-[titleLabel]-|", options: nil, metrics: metrics, views: views))
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(margin)-[titleLabel]-(margin)-|", options: nil, metrics: metrics, views: views))
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[lineView]", options: nil, metrics: metrics, views: views))
contentView.addConstraint(NSLayoutConstraint(item: circleView, attribute: .CenterX, relatedBy: .Equal, toItem: lineView, attribute: .CenterX, multiplier: 1, constant: 0))
contentView.addConstraint(NSLayoutConstraint(item: circleView, attribute: .CenterY, relatedBy: .Equal, toItem: titleLabel, attribute: .CenterY, multiplier: 1, constant: 0))
contentView.addConstraint(NSLayoutConstraint(item: circleView, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 14))
contentView.addConstraint(NSLayoutConstraint(item: circleView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 14))
contentView.addConstraint(NSLayoutConstraint(item: titleLabel, attribute: .CenterY, relatedBy: .Equal, toItem: timeLabel, attribute: .CenterY, multiplier: 1, constant: 0))
contentView.addConstraint(lineViewYConstraint)
contentView.addConstraint(heightConstraint)
}
func animateTimeLabelTextChange(text: String) {
let animation = CATransition();
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.type = kCATransitionFade;
animation.duration = 0.15;
timeLabel.layer.addAnimation(animation, forKey: "kCATransitionFade")
timeLabel.text = text
}
}
extension UIColor {
class func selectedGreen() -> UIColor {
return UIColor(red: 0.541, green: 0.875, blue: 0.780, alpha: 1)
}
class func disabledGreen() -> UIColor {
return UIColor(red: 0.329, green: 0.831, blue: 0.690, alpha: 1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment