Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Digital Clock Widget #181

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions GrandCentralBoard.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@
5435EFF31CEBDD67002A9869 /* WatchWidgetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5435EFF21CEBDD67002A9869 /* WatchWidgetViewModelTests.swift */; };
547A45831D018E07004E6504 /* BillableDatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547A45821D018E07004E6504 /* BillableDatesTests.swift */; };
548AB84A1CEF39170086A1EC /* WebsiteAnalyticsWidgetSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548AB8491CEF39170086A1EC /* WebsiteAnalyticsWidgetSnapshotTests.swift */; };
5DAA9C351D07089500AAE153 /* DigitalClockSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C2E1D07089500AAE153 /* DigitalClockSource.swift */; };
5DAA9C361D07089500AAE153 /* DigitalClockWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C2F1D07089500AAE153 /* DigitalClockWidget.swift */; };
5DAA9C371D07089500AAE153 /* DigitalClockWidgetBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C301D07089500AAE153 /* DigitalClockWidgetBuilder.swift */; };
5DAA9C381D07089500AAE153 /* DigitalClockWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C311D07089500AAE153 /* DigitalClockWidgetViewModel.swift */; };
5DAA9C391D07089500AAE153 /* DigitalClockWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C331D07089500AAE153 /* DigitalClockWidgetView.swift */; };
5DAA9C3A1D07089500AAE153 /* DigitalClockWidgetView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5DAA9C341D07089500AAE153 /* DigitalClockWidgetView.xib */; };
5DAA9C3D1D0708BA00AAE153 /* DigitalClockWidgetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C3C1D0708BA00AAE153 /* DigitalClockWidgetViewModelTests.swift */; };
73446E280D300467806A41C0 /* Pods_GrandCentralBoardTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1BE02EF1A91C166A60B79697 /* Pods_GrandCentralBoardTests.framework */; };
89EA2514DC4CC9CE2261B3D9 /* Pods_GrandCentralBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54A9A8A20231B63CE5AC43BA /* Pods_GrandCentralBoard.framework */; };
C110E0D31CD778CB00B19DC6 /* pull_requests.json in Resources */ = {isa = PBXBuildFile; fileRef = C110E0D21CD778CB00B19DC6 /* pull_requests.json */; };
Expand Down Expand Up @@ -326,6 +333,13 @@
547A45821D018E07004E6504 /* BillableDatesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BillableDatesTests.swift; sourceTree = "<group>"; };
548AB8491CEF39170086A1EC /* WebsiteAnalyticsWidgetSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebsiteAnalyticsWidgetSnapshotTests.swift; sourceTree = "<group>"; };
54A9A8A20231B63CE5AC43BA /* Pods_GrandCentralBoard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GrandCentralBoard.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5DAA9C2E1D07089500AAE153 /* DigitalClockSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DigitalClockSource.swift; sourceTree = "<group>"; };
5DAA9C2F1D07089500AAE153 /* DigitalClockWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DigitalClockWidget.swift; sourceTree = "<group>"; };
5DAA9C301D07089500AAE153 /* DigitalClockWidgetBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DigitalClockWidgetBuilder.swift; sourceTree = "<group>"; };
5DAA9C311D07089500AAE153 /* DigitalClockWidgetViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DigitalClockWidgetViewModel.swift; sourceTree = "<group>"; };
5DAA9C331D07089500AAE153 /* DigitalClockWidgetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DigitalClockWidgetView.swift; sourceTree = "<group>"; };
5DAA9C341D07089500AAE153 /* DigitalClockWidgetView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DigitalClockWidgetView.xib; sourceTree = "<group>"; };
5DAA9C3C1D0708BA00AAE153 /* DigitalClockWidgetViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DigitalClockWidgetViewModelTests.swift; sourceTree = "<group>"; };
7D608D9A9B4291AEBF9C7FD7 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; };
909F842DBEAB5F13BDC41BB8 /* Pods-GrandCentralBoardTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GrandCentralBoardTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-GrandCentralBoardTests/Pods-GrandCentralBoardTests.release.xcconfig"; sourceTree = "<group>"; };
BF37772EDE6EC78426FE7E75 /* Pods-GrandCentralBoard.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GrandCentralBoard.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GrandCentralBoard/Pods-GrandCentralBoard.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -441,6 +455,7 @@
0D2863071CB515B000C62700 /* Widgets */ = {
isa = PBXGroup;
children = (
5DAA9C3B1D0708BA00AAE153 /* DigitalClock */,
52B9C3D31CEF38C300EE6BD0 /* Image */,
520B7A6A1CE4A969005F53EC /* Slack */,
526FC24B1CD0E74700D7843A /* GitHub */,
Expand Down Expand Up @@ -520,6 +535,7 @@
1A242D561C357DFD00D5BEE5 /* Widgets */ = {
isa = PBXGroup;
children = (
5DAA9C2D1D07089500AAE153 /* Digital Clock */,
520B7A591CE36ACC005F53EC /* Slack */,
526FC2411CD0ADCC00D7843A /* GitHub */,
067DA5411CBE5CE400048E6A /* WebsiteAnalytics */,
Expand Down Expand Up @@ -920,6 +936,36 @@
name = WebsiteAnalytics;
sourceTree = "<group>";
};
5DAA9C2D1D07089500AAE153 /* Digital Clock */ = {
isa = PBXGroup;
children = (
5DAA9C2E1D07089500AAE153 /* DigitalClockSource.swift */,
5DAA9C2F1D07089500AAE153 /* DigitalClockWidget.swift */,
5DAA9C301D07089500AAE153 /* DigitalClockWidgetBuilder.swift */,
5DAA9C311D07089500AAE153 /* DigitalClockWidgetViewModel.swift */,
5DAA9C321D07089500AAE153 /* View */,
);
path = "Digital Clock";
sourceTree = "<group>";
};
5DAA9C321D07089500AAE153 /* View */ = {
isa = PBXGroup;
children = (
5DAA9C331D07089500AAE153 /* DigitalClockWidgetView.swift */,
5DAA9C341D07089500AAE153 /* DigitalClockWidgetView.xib */,
);
path = View;
sourceTree = "<group>";
};
5DAA9C3B1D0708BA00AAE153 /* DigitalClock */ = {
isa = PBXGroup;
children = (
5DAA9C3C1D0708BA00AAE153 /* DigitalClockWidgetViewModelTests.swift */,
);
name = DigitalClock;
path = Widgets/DigitalClock;
sourceTree = "<group>";
};
C14EA2EA1CF061E500961DF6 /* View */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1106,6 +1152,7 @@
buildActionMask = 2147483647;
files = (
521B68511CF58D5400E112CC /* NoOpenPRPlaceholder.xib in Resources */,
5DAA9C3A1D07089500AAE153 /* DigitalClockWidgetView.xib in Resources */,
521B684D1CF4700600E112CC /* Assets.xcassets in Resources */,
494B68CD1C92E89A00D460B0 /* BonusWidgetView.xib in Resources */,
529CEF0A1CEC6A2200636A4A /* HarvestWidgetView.xib in Resources */,
Expand Down Expand Up @@ -1257,6 +1304,7 @@
0662F2661CC046BF00729093 /* Calendar.swift in Sources */,
067DA5471CBE5D7B00048E6A /* WebsiteAnalyticsWidget.swift in Sources */,
06684F2B1CB788FB00B93D90 /* BonuslyRequestTemplates.swift in Sources */,
5DAA9C351D07089500AAE153 /* DigitalClockSource.swift in Sources */,
F91F007D1CBBDF2200DAAA77 /* TokenRefreshCredentials.swift in Sources */,
494B68CE1C92E89A00D460B0 /* BonusWidgetViewModel.swift in Sources */,
52FB50851CBFB5380053DE8B /* ImageWidgetBuilder.swift in Sources */,
Expand Down Expand Up @@ -1330,20 +1378,24 @@
0D7970C51CB6B01F00401A65 /* BubbleScalingAnimator.swift in Sources */,
1AD6E8031C7DDC000003121C /* WatchWidgetBuilder.swift in Sources */,
067DA55D1CBFA8F900048E6A /* TableViewCell.swift in Sources */,
5DAA9C391D07089500AAE153 /* DigitalClockWidgetView.swift in Sources */,
1AD5EAA41C7D125A00210BB9 /* WatchWidget.swift in Sources */,
526D1C671CE4BA2000604174 /* String+SlackTimestampParsing.swift in Sources */,
F9E920891CC5421200BC7994 /* BillingProjectListFetcher.swift in Sources */,
06684F231CB69EB100B93D90 /* PeopleWithBonusesFetchController.swift in Sources */,
529CEF191CECC35600636A4A /* UIColor+BillingColor.swift in Sources */,
529CEF041CEC580800636A4A /* CircleChartViewModel.swift in Sources */,
5DAA9C361D07089500AAE153 /* DigitalClockWidget.swift in Sources */,
0692DB911CBD39DC006F46C9 /* Array.swift in Sources */,
1A242D211C357C2700D5BEE5 /* AppDelegate.swift in Sources */,
5DAA9C381D07089500AAE153 /* DigitalClockWidgetViewModel.swift in Sources */,
494B68DA1C92E89A00D460B0 /* BonusSource.swift in Sources */,
1A87495E1C385264006E58C6 /* RemoteImageSource.swift in Sources */,
F9C88F3E1CBFC72100E81A14 /* AccessTokenFetcher.swift in Sources */,
52FB50621CBE862D0053DE8B /* EventsSource.swift in Sources */,
F928B82E1CC52F77006D0332 /* String+Localized.swift in Sources */,
494B68C91C92E89A00D460B0 /* BonusModels.swift in Sources */,
5DAA9C371D07089500AAE153 /* DigitalClockWidgetBuilder.swift in Sources */,
C1719D5D1CF16B80009B52BA /* GitHubSource.swift in Sources */,
F91F006C1CBBD02600DAAA77 /* HarvestWidget.swift in Sources */,
F9E9208F1CC54A9400BC7994 /* DailyUserBillingStatsFetcher.swift in Sources */,
Expand Down Expand Up @@ -1377,6 +1429,7 @@
547A45831D018E07004E6504 /* BillableDatesTests.swift in Sources */,
526FC2601CD365E300D7843A /* UpdateTests.swift in Sources */,
1A242D331C357C2700D5BEE5 /* GrandCentralBoardTests.swift in Sources */,
5DAA9C3D1D0708BA00AAE153 /* DigitalClockWidgetViewModelTests.swift in Sources */,
1A4616AA1C904EAB00361B2E /* ConfigurationTests.swift in Sources */,
1AB810F31CCA5DD800E3A430 /* ConfigurationRefresherTests.swift in Sources */,
5250277F1CDCE92E00DFE32A /* BillingProjectListTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ final class MainViewController: UIViewController {
ImageWidgetBuilder(dataDownloader: dataDownloader),
BlogPostsPopularityWidgetBuilder(),
SlackWidgetBuilder(),
GitHubWidgetBuilder()
GitHubWidgetBuilder(),
DigitalClockWidgetBuilder()
]

if shouldLoadBundledConfig {
Expand Down
47 changes: 47 additions & 0 deletions GrandCentralBoard/Widgets/Digital Clock/DigitalClockSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// DigitalClockSource.swift
// GrandCentralBoard
//
// Created by Joel Fischer on 4/22/16.
// Copyright © 2016 Oktawian Chojnacki. All rights reserved.
//

import UIKit

import Decodable
import GCBCore

/**
* { settings: { "timeZone": "America/Detroit"} }
* Available Timezones here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
*/
struct DigitalClockSourceSettings: Decodable {
let timeZone: NSTimeZone
let layout: String?
let clockType: String?

static func decode(jsonObject: AnyObject) throws -> DigitalClockSourceSettings {
return try DigitalClockSourceSettings(timeZone: NSTimeZone(name: jsonObject => "timeZone") ??
NSTimeZone.defaultTimeZone(),
layout: jsonObject =>? "layout",
clockType: jsonObject =>? "type")
}
}

final class DigitalClockSource: Synchronous {

typealias ResultType = Result<Time>

let interval: NSTimeInterval = 1
let sourceType: SourceType = .Momentary

private let timeZone: NSTimeZone

init(settings: DigitalClockSourceSettings) {
self.timeZone = settings.timeZone
}

func read() -> ResultType {
return .Success(Time(time: NSDate(), timeZone: timeZone))
}
}
55 changes: 55 additions & 0 deletions GrandCentralBoard/Widgets/Digital Clock/DigitalClockWidget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// clockWidget.swift
// GrandCentralBoard
//
// Created by Joel Fischer on 4/22/16.
// Copyright © 2016 Oktawian Chojnacki. All rights reserved.
//

import UIKit
import GCBCore

final class DigitalClockWidget: WidgetControlling {
private let widgetView: DigitalClockWidgetView
private let mainView: UIView

let sources: [UpdatingSource]

init(view: DigitalClockWidgetView, sources: [UpdatingSource]) {
self.widgetView = view
self.sources = sources

mainView = UIView(frame: widgetView.frame)
mainView.fillViewWithView(widgetView, animated: false)
}

var view: UIView {
return widgetView
}

private var lastFetch: NSDate?
func update(source: UpdatingSource) {
switch source {
case let source as DigitalClockSource:
updateTimeFromSource(source)
default:
assertionFailure("Expected `source` as instance of `TimeSource`.")
}
}

private func updateTimeFromSource(source: DigitalClockSource) {
let result = source.read()

switch result {
case .Success(let time):
renderTime(time)
case .Failure:
widgetView.failure()
}
}

private func renderTime(time: Time) {
let timeViewModel = DigitalClockWidgetViewModel(date: time.time, timeZone: time.timeZone)
widgetView.render(timeViewModel)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// DigitalClockWidgetBuilder.swift
// GrandCentralBoard
//
// Created by Joel Fischer on 4/22/16.
// Copyright © 2016 Oktawian Chojnacki. All rights reserved.
//

import UIKit
import GCBCore

final class DigitalClockWidgetBuilder: WidgetBuilding {
let name = "digital clock"

func build(settings: AnyObject) throws -> WidgetControlling {
let clockSettings = try DigitalClockSourceSettings.decode(settings)
let clockSource = DigitalClockSource(settings: clockSettings)

let view = DigitalClockWidgetView.fromNib()
return DigitalClockWidget(view: view, sources: [clockSource])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// clockViewModel.swift
// GrandCentralBoard
//
// Created by Joel Fischer on 4/22/16.
// Copyright © 2016 Oktawian Chojnacki. All rights reserved.
//

import UIKit
import GCBCore

struct DigitalClockWidgetViewModel {
let timeString: String
let dateString: String
let timeZoneCityName: String

private static let timeFormatter: NSDateFormatter = {
let timeFormatter = NSDateFormatter()
timeFormatter.dateFormat = "hh:mm:ss a"
timeFormatter.locale = NSLocale.currentLocale()

return timeFormatter
}()

private static let dateFormatter: NSDateFormatter = {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "EEEE, MMMM dd, yyyy"
dateFormatter.locale = NSLocale.currentLocale()

return dateFormatter
}()

init(date: NSDate, timeZone: NSTimeZone) {
timeString = DigitalClockWidgetViewModel.timeFormatter.stringFromDate(date)
dateString = DigitalClockWidgetViewModel.dateFormatter.stringFromDate(date)
timeZoneCityName = DigitalClockWidgetViewModel.zoneNameForTimeZone(timeZone)
}

private static func zoneNameForTimeZone(timeZone: NSTimeZone) -> String {
let string = timeZone.name.characters.split("/").map(String.init).last
return string!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// clockView.swift
// GrandCentralBoard
//
// Created by Joel Fischer on 4/22/16.
// Copyright © 2016 Oktawian Chojnacki. All rights reserved.
//

import UIKit
import GCBCore

final class DigitalClockWidgetView: UIView, ViewModelRendering {
@IBOutlet weak var clockLabel: UILabel!
@IBOutlet weak var timeZoneLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!

typealias ViewModel = DigitalClockWidgetViewModel

private var useMilitaryTime: Bool = false
private(set) var state: RenderingState<ViewModel> = .Waiting {
didSet { handleTransitionFromState(oldValue, toState: state) }
}

func render(viewModel: ViewModel) {
state = .Rendering(viewModel)
}

func failure() {
state = .Failed
}


// MARK - Transitions

private func handleTransitionFromState(state: RenderingState<ViewModel>?, toState: RenderingState<ViewModel>) {
switch (state, toState) {
case (_, .Rendering(let viewModel)):
updateLabelsWithViewModel(viewModel)
default:
break
}
}

private func updateLabelsWithViewModel(viewModel: ViewModel) {
clockLabel.text = viewModel.timeString
timeZoneLabel.text = viewModel.timeZoneCityName
dateLabel.text = viewModel.dateString
}


// MARK - fromNib

class func fromNib() -> DigitalClockWidgetView {
return NSBundle.mainBundle().loadNibNamed("DigitalClockWidgetView", owner: nil, options: nil)[0] as! DigitalClockWidgetView
}
}
Loading