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

Analog Clock Widget #180

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.
Jump to
Jump to file
Failed to load files.
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 */; };
5DAA9C401D0709E800AAE153 /* AnalogClockViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C3F1D0709E800AAE153 /* AnalogClockViewModelTests.swift */; };
5DAA9C491D0709ED00AAE153 /* AnalogClockSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C421D0709ED00AAE153 /* AnalogClockSource.swift */; };
5DAA9C4A1D0709ED00AAE153 /* AnalogClockWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C431D0709ED00AAE153 /* AnalogClockWidget.swift */; };
5DAA9C4B1D0709ED00AAE153 /* AnalogClockWidgetBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C441D0709ED00AAE153 /* AnalogClockWidgetBuilder.swift */; };
5DAA9C4C1D0709ED00AAE153 /* AnalogClockWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C451D0709ED00AAE153 /* AnalogClockWidgetViewModel.swift */; };
5DAA9C4D1D0709ED00AAE153 /* AnalogClockWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DAA9C471D0709ED00AAE153 /* AnalogClockWidgetView.swift */; };
5DAA9C4E1D0709ED00AAE153 /* AnalogClockWidgetView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5DAA9C481D0709ED00AAE153 /* AnalogClockWidgetView.xib */; };
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; };
5DAA9C3F1D0709E800AAE153 /* AnalogClockViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalogClockViewModelTests.swift; sourceTree = "<group>"; };
5DAA9C421D0709ED00AAE153 /* AnalogClockSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalogClockSource.swift; sourceTree = "<group>"; };
5DAA9C431D0709ED00AAE153 /* AnalogClockWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalogClockWidget.swift; sourceTree = "<group>"; };
5DAA9C441D0709ED00AAE153 /* AnalogClockWidgetBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalogClockWidgetBuilder.swift; sourceTree = "<group>"; };
5DAA9C451D0709ED00AAE153 /* AnalogClockWidgetViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalogClockWidgetViewModel.swift; sourceTree = "<group>"; };
5DAA9C471D0709ED00AAE153 /* AnalogClockWidgetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalogClockWidgetView.swift; sourceTree = "<group>"; };
5DAA9C481D0709ED00AAE153 /* AnalogClockWidgetView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AnalogClockWidgetView.xib; 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 = (
5DAA9C3E1D0709E800AAE153 /* Analog Clock */,
52B9C3D31CEF38C300EE6BD0 /* Image */,
520B7A6A1CE4A969005F53EC /* Slack */,
526FC24B1CD0E74700D7843A /* GitHub */,
Expand Down Expand Up @@ -520,6 +535,7 @@
1A242D561C357DFD00D5BEE5 /* Widgets */ = {
isa = PBXGroup;
children = (
5DAA9C411D0709ED00AAE153 /* Analog Clock */,
520B7A591CE36ACC005F53EC /* Slack */,
526FC2411CD0ADCC00D7843A /* GitHub */,
067DA5411CBE5CE400048E6A /* WebsiteAnalytics */,
Expand Down Expand Up @@ -920,6 +936,36 @@
name = WebsiteAnalytics;
sourceTree = "<group>";
};
5DAA9C3E1D0709E800AAE153 /* Analog Clock */ = {
isa = PBXGroup;
children = (
5DAA9C3F1D0709E800AAE153 /* AnalogClockViewModelTests.swift */,
);
name = "Analog Clock";
path = "Widgets/Analog Clock";
sourceTree = "<group>";
};
5DAA9C411D0709ED00AAE153 /* Analog Clock */ = {
isa = PBXGroup;
children = (
5DAA9C421D0709ED00AAE153 /* AnalogClockSource.swift */,
5DAA9C431D0709ED00AAE153 /* AnalogClockWidget.swift */,
5DAA9C441D0709ED00AAE153 /* AnalogClockWidgetBuilder.swift */,
5DAA9C451D0709ED00AAE153 /* AnalogClockWidgetViewModel.swift */,
5DAA9C461D0709ED00AAE153 /* View */,
);
path = "Analog Clock";
sourceTree = "<group>";
};
5DAA9C461D0709ED00AAE153 /* View */ = {
isa = PBXGroup;
children = (
5DAA9C471D0709ED00AAE153 /* AnalogClockWidgetView.swift */,
5DAA9C481D0709ED00AAE153 /* AnalogClockWidgetView.xib */,
);
path = View;
sourceTree = "<group>";
};
C14EA2EA1CF061E500961DF6 /* View */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1113,6 +1159,7 @@
067DA55F1CBFA98F00048E6A /* TableViewCell.xib in Resources */,
C14EA2EC1CF0728000961DF6 /* GitHubCell.xib in Resources */,
1A4616A61C9048F700361B2E /* Assets.xcassets in Resources */,
5DAA9C4E1D0709ED00AAE153 /* AnalogClockWidgetView.xib in Resources */,
1AD5EA9D1C7B8FED00210BB9 /* Assets.xcassets in Resources */,
1A242D261C357C2700D5BEE5 /* Main.storyboard in Resources */,
1AA97DE51CA2DE720058DBCE /* configuration.json in Resources */,
Expand Down Expand Up @@ -1286,12 +1333,14 @@
526FC2431CD0ADF300D7843A /* GitHubDataProvider.swift in Sources */,
1A76DFAE1CBBEF8200A2DF49 /* NSBundle.swift in Sources */,
F91F006E1CBBD08500DAAA77 /* HarvestWidgetBuilder.swift in Sources */,
5DAA9C4B1D0709ED00AAE153 /* AnalogClockWidgetBuilder.swift in Sources */,
1AE1981E1CBE8323003B6AB3 /* UIColor.swift in Sources */,
52FB50581CBD350E0053DE8B /* GoogleCalendarWatchWidgetBuilder.swift in Sources */,
C14EA2EE1CF072A500961DF6 /* GitHubCell.swift in Sources */,
1AEE4C0A1CC52E41000CC88D /* NSProcessInfo.swift in Sources */,
494B68D01C92E89A00D460B0 /* BonusScene.swift in Sources */,
F9E920911CC54BF000BC7994 /* NSDate+StringWithFormat.swift in Sources */,
5DAA9C491D0709ED00AAE153 /* AnalogClockSource.swift in Sources */,
065588D11CC5001300BF2DF9 /* Dictionary.swift in Sources */,
52FB50631CBE862F0053DE8B /* CalendarNameSource.swift in Sources */,
1AD5EA991C7B827300210BB9 /* WatchWidgetView.swift in Sources */,
Expand All @@ -1300,6 +1349,7 @@
F9C88F3A1CBFC36900E81A14 /* BillingStatsFetcher.swift in Sources */,
06684F321CB7896800B93D90 /* TimestampableRequestTemplate.swift in Sources */,
F9C88F2F1CBE991C00E81A14 /* BillingStatsGroup.swift in Sources */,
5DAA9C4D1D0709ED00AAE153 /* AnalogClockWidgetView.swift in Sources */,
520B7A671CE38B68005F53EC /* String.swift in Sources */,
C1719D5F1CF184C4009B52BA /* GitHubWidgetView.swift in Sources */,
520B7A631CE37B56005F53EC /* SlackWidgetBuilder.swift in Sources */,
Expand Down Expand Up @@ -1330,11 +1380,13 @@
0D7970C51CB6B01F00401A65 /* BubbleScalingAnimator.swift in Sources */,
1AD6E8031C7DDC000003121C /* WatchWidgetBuilder.swift in Sources */,
067DA55D1CBFA8F900048E6A /* TableViewCell.swift in Sources */,
5DAA9C4C1D0709ED00AAE153 /* AnalogClockWidgetViewModel.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 */,
5DAA9C4A1D0709ED00AAE153 /* AnalogClockWidget.swift in Sources */,
529CEF041CEC580800636A4A /* CircleChartViewModel.swift in Sources */,
0692DB911CBD39DC006F46C9 /* Array.swift in Sources */,
1A242D211C357C2700D5BEE5 /* AppDelegate.swift in Sources */,
Expand Down Expand Up @@ -1380,6 +1432,7 @@
1A4616AA1C904EAB00361B2E /* ConfigurationTests.swift in Sources */,
1AB810F31CCA5DD800E3A430 /* ConfigurationRefresherTests.swift in Sources */,
5250277F1CDCE92E00DFE32A /* BillingProjectListTests.swift in Sources */,
5DAA9C401D0709E800AAE153 /* AnalogClockViewModelTests.swift in Sources */,
491ADF261CF352FB00A13978 /* BonusWidgetSnapshotTests.swift in Sources */,
C1719D551CF07A0C009B52BA /* GitHubCellSnapshotTests.swift in Sources */,
52B9C3D01CEF258B00EE6BD0 /* HarvestWidgetViewSnapshotTests.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(),
AnalogClockWidgetBuilder()
]

if shouldLoadBundledConfig {
Expand Down
44 changes: 44 additions & 0 deletions GrandCentralBoard/Widgets/Analog Clock/AnalogClockSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// AnalogClockSource.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 AnalogAnalogClockSourceSettings: Decodable {
let timeZone: NSTimeZone

static func decode(jsonObject: AnyObject) throws -> AnalogAnalogClockSourceSettings {
return try AnalogAnalogClockSourceSettings(timeZone: NSTimeZone(name: jsonObject => "timeZone") ??
NSTimeZone.defaultTimeZone())
}
}

final class AnalogClockSource: Synchronous {

typealias ResultType = Result<Time>

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

private let timeZone: NSTimeZone

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

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

import UIKit
import GCBCore

final class AnalogClockWidget: WidgetControlling {
private let widgetView: AnalogClockWidgetView
private let mainView: UIView

let sources: [UpdatingSource]

init(view: AnalogClockWidgetView, 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 AnalogClockSource:
updateTimeFromSource(source)
default:
assertionFailure("Expected `source` as instance of `TimeSource`.")
}
}

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

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

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

import UIKit
import GCBCore

final class AnalogClockWidgetBuilder: WidgetBuilding {
let name = "analog clock"

func build(settings: AnyObject) throws -> WidgetControlling {
let clockSettings = try AnalogAnalogClockSourceSettings.decode(settings)
let source = AnalogClockSource(settings: clockSettings)
let view = AnalogClockWidgetView.fromNib()

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

import UIKit
import GCBCore

struct AnalogClockWidgetViewModel {
let hour: Int
let minute: Int
let second: Int

let timeZoneCityName: String

let dateString: String // From NSDateFormatter .LongStyle

init(date: NSDate, timeZone: NSTimeZone) {
let dateComponents = AnalogClockWidgetViewModel.componentsFromDate(date, timeZone: timeZone)
hour = dateComponents.hour
minute = dateComponents.minute
second = dateComponents.second

timeZoneCityName = AnalogClockWidgetViewModel.zoneNameForTimeZone(timeZone)
dateString = AnalogClockWidgetViewModel.stringForDate(date)
}

private static func zoneNameForTimeZone(timeZone: NSTimeZone) -> String {
let string = timeZone.name.characters.split("/").map(String.init).last
return string!
}

private static func componentsFromDate(date: NSDate, timeZone: NSTimeZone) -> NSDateComponents {
let calendar = NSCalendar.currentCalendar()
calendar.timeZone = timeZone
return calendar.components([.Hour, .Minute, .Second], fromDate: date)
}

private static func stringForDate(date: NSDate) -> String {
let dateFormatter = NSDateFormatter()
dateFormatter.dateStyle = .LongStyle
dateFormatter.timeStyle = .NoStyle
dateFormatter.locale = NSLocale.currentLocale()

return dateFormatter.stringFromDate(date)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// AnalogClockWidgetView.swift
// GrandCentralBoard
//
// Created by Joel Fischer on 4/25/16.
// Copyright © 2016 Oktawian Chojnacki. All rights reserved.
//

import UIKit
import GCBCore
import BEMAnalogClock

final class AnalogClockWidgetView: UIView, ViewModelRendering {
@IBOutlet weak var analogClockView: BEMAnalogClockView!
@IBOutlet weak var timeZoneLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!

typealias ViewModel = AnalogClockWidgetViewModel

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)
updateAnalogClockWithViewModel(viewModel)
default:
break
}
}

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

private func updateAnalogClockWithViewModel(viewModel: ViewModel) {
analogClockView.hours = viewModel.hour
analogClockView.minutes = viewModel.minute
analogClockView.seconds = viewModel.second
analogClockView.updateTimeAnimated(false)
}




// MARK - fromNib

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