├── images
├── Main.png
├── Widget.png
├── Notification.png
├── GitBingoDiagram.png
├── NotificationAlert.png
└── TodayExtensionDiagram.png
├── GitBingo
├── Assets.xcassets
│ ├── Contents.json
│ ├── sync.imageset
│ │ ├── sync@1x.png
│ │ ├── sync@2x.png
│ │ ├── sync@3x.png
│ │ └── Contents.json
│ ├── icons8-grid.imageset
│ │ ├── icons8-grid.png
│ │ ├── icons8-grid-1.png
│ │ ├── icons8-grid-2.png
│ │ └── Contents.json
│ ├── icons8-time.imageset
│ │ ├── icons8-time.png
│ │ ├── icons8-timer.png
│ │ ├── icons8-timer-1.png
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-57x57@1x.png
│ │ ├── Icon-App-57x57@2x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-72x72@1x.png
│ │ ├── Icon-App-72x72@2x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ ├── ItunesArtwork@2x.png
│ │ ├── Icon-Small-50x50@1x.png
│ │ ├── Icon-Small-50x50@2x.png
│ │ ├── Icon-App-83.5x83.5@2x.png
│ │ └── Contents.json
│ └── icons8-trash.imageset
│ │ ├── icons8-trash@1x.png
│ │ ├── icons8-trash@2x.png
│ │ ├── icons8-trash@3x.png
│ │ └── Contents.json
├── Localizing
│ ├── en.lproj
│ │ └── Localizable.strings
│ └── ko.lproj
│ │ └── Localizable.strings
├── Helper
│ ├── RefreshMode.swift
│ ├── KeyIdentifier.swift
│ ├── AppURL.swift
│ ├── GitBingoError.swift
│ └── GitBingoAlertState.swift
├── Dependencies
│ ├── GithubIdReceiver.swift
│ ├── SectionModelsReceiver.swift
│ ├── ContributionReceiver.swift
│ ├── ContributionsMapper.swift
│ └── HomeViewDependencyContainer.swift
├── Service
│ ├── Parser
│ │ ├── HTMLParsingProtocol.swift
│ │ └── Parser.swift
│ ├── APIService
│ │ ├── SessionManagerProtocol.swift
│ │ └── APIService.swift
│ └── GroupUserDefaults.swift
├── GitBingo.entitlements
├── View
│ └── SectionHeaderView.swift
├── Extensions
│ ├── ReusableIdentifier.swift
│ ├── UIColorExtensions.swift
│ ├── Storyboarded.swift
│ ├── LocalizedString.swift
│ └── CustomAlert.swift
├── ko.lproj
│ └── LaunchScreen.strings
├── ViewModel
│ ├── Home
│ │ └── HomeViewModel.swift
│ └── IDInputVIewModel
│ │ └── IDInputViewModel.swift
├── Model
│ ├── Dot.swift
│ ├── ContributionGrade.swift
│ └── Contributions.swift
├── Info.plist
├── ViewControllers
│ ├── RegisterAlert
│ │ └── RegisterAlertViewController.swift
│ ├── IDInput
│ │ ├── IDInputViewController.swift
│ │ └── IDInputViewController.storyboard
│ └── Home
│ │ └── HomeViewController.swift
├── Presenters
│ ├── MainViewPresenter.swift
│ └── RegisterViewPresenter.swift
├── AppDelegate.swift
├── RegisterAlertViewController.storyboard
├── Base.lproj
│ └── LaunchScreen.storyboard
└── HomeViewController.storyboard
├── .swiftlint.yml
├── GitBingo.xcworkspace
└── contents.xcworkspacedata
├── GitBingo Widget
├── GitBingo Widget.entitlements
├── Info.plist
├── en.lproj
│ └── MainInterface.strings
├── ko.lproj
│ └── MainInterface.strings
├── TodayViewPresenter.swift
├── TodayViewController.swift
└── Base.lproj
│ └── MainInterface.storyboard
├── Podfile
├── GitBingoTests
├── SessionManagerStub.swift
├── Info.plist
├── MainViewStub.swift
├── APIServiceStub.swift
├── GitBingoTests.swift
└── GitBingoPresenterTests.swift
├── Podfile.lock
├── README.md
├── .gitignore
└── Privacy Policy.md
/images/Main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/images/Main.png
--------------------------------------------------------------------------------
/images/Widget.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/images/Widget.png
--------------------------------------------------------------------------------
/images/Notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/images/Notification.png
--------------------------------------------------------------------------------
/images/GitBingoDiagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/images/GitBingoDiagram.png
--------------------------------------------------------------------------------
/images/NotificationAlert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/images/NotificationAlert.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/images/TodayExtensionDiagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/images/TodayExtensionDiagram.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/sync.imageset/sync@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/sync.imageset/sync@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/sync.imageset/sync@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/sync.imageset/sync@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/sync.imageset/sync@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/sync.imageset/sync@3x.png
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - line_length
3 | - identifier_name
4 | - trailing_whitespace
5 | - multiple_closures_with_trailing_closure
6 | excluded:
7 | - Pods
8 |
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-grid.imageset/icons8-grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-grid.imageset/icons8-grid.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-time.imageset/icons8-time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-time.imageset/icons8-time.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-grid.imageset/icons8-grid-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-grid.imageset/icons8-grid-1.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-grid.imageset/icons8-grid-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-grid.imageset/icons8-grid-2.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-time.imageset/icons8-timer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-time.imageset/icons8-timer.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-time.imageset/icons8-timer-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-time.imageset/icons8-timer-1.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-trash.imageset/icons8-trash@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-trash.imageset/icons8-trash@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-trash.imageset/icons8-trash@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-trash.imageset/icons8-trash@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-trash.imageset/icons8-trash@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/icons8-trash.imageset/icons8-trash@3x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/protocorn93/GitBingo/HEAD/GitBingo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/GitBingo/Localizing/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | GitBingo
4 |
5 | Created by 이동건 on 05/09/2018.
6 | Copyright © 2018 이동건. All rights reserved.
7 | */
8 |
--------------------------------------------------------------------------------
/GitBingo/Helper/RefreshMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RefreshMode.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 01/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum RefreshMode {
12 | case pullToRefresh
13 | case tapToRefresh
14 | }
15 |
--------------------------------------------------------------------------------
/GitBingo.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/GitBingo/Dependencies/GithubIdReceiver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GithubIdReceiver.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 18/07/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 |
11 | protocol GithubIdReceiver {
12 | var githubID: BehaviorSubject { get }
13 | }
14 |
--------------------------------------------------------------------------------
/GitBingo/Service/Parser/HTMLParsingProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTMLParsingProtocol.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 29/11/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol HTMLParsingProtocol: class {
12 | func parse(from data: Data?) -> Contributions?
13 | }
14 |
--------------------------------------------------------------------------------
/GitBingo/GitBingo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.Gitbingo
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/GitBingo/View/SectionHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SectionHeaderView.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 02/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SectionHeaderView: UICollectionReusableView {
12 | // MARK: Outlets
13 | @IBOutlet weak var weekLabel: UILabel!
14 | }
15 |
--------------------------------------------------------------------------------
/GitBingo Widget/GitBingo Widget.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.Gitbingo
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/GitBingo/Extensions/ReusableIdentifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReusableIdentifier.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 02/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension NSObject {
12 | static var reusableIdentifier: String {
13 | return String(describing: self)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/GitBingo/Dependencies/SectionModelsReceiver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SectionModelsReceiver.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 18/07/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 | import RxDataSources
11 |
12 | protocol SectionModelsReceiver {
13 | var sectionModels: PublishSubject<[SectionModel]> { get }
14 | }
15 |
--------------------------------------------------------------------------------
/GitBingo/Extensions/UIColorExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColorExtensions.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 24/08/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIColor {
12 | convenience init(hex: Int) {
13 | self.init(red: CGFloat((hex & 0xFF0000) >> 16) / 255.0, green: CGFloat((hex & 0x00FF00) >> 8) / 255.0, blue: CGFloat(hex & 0x0000FF) / 255.0, alpha: 1)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/GitBingo/Service/APIService/SessionManagerProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SessionManagerProtocol.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 29/11/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol SessionManagerProtocol {
12 | func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
13 | }
14 |
15 | extension URLSession: SessionManagerProtocol {}
16 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | platform :ios, '10.0'
3 |
4 | target 'GitBingo' do
5 | use_frameworks!
6 | pod 'Kanna'
7 | pod 'SVProgressHUD'
8 | pod 'SwiftLint'
9 | pod 'RxSwift'
10 | pod 'RxCocoa'
11 | pod 'RxDataSources'
12 | end
13 |
14 | target 'GitBingo Widget' do
15 | use_frameworks!
16 | pod 'Kanna'
17 | pod 'SwiftLint'
18 | pod 'RxSwift'
19 | pod 'RxCocoa'
20 | pod 'RxDataSources'
21 | end
22 |
--------------------------------------------------------------------------------
/GitBingo/Helper/KeyIdentifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyIdentifier.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 03/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum KeyIdentifier {
12 | case id
13 | case notification
14 |
15 | var value: String {
16 | switch self {
17 | case .id:
18 | return "id"
19 | case .notification:
20 | return "notification"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/sync.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "sync@1x.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "sync@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "sync@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/GitBingo/Helper/AppURL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppURL.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 27/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum AppURL {
12 | case authentication
13 | case notificaiton
14 |
15 | var path: String {
16 | switch self {
17 | case .authentication:
18 | return "authentication"
19 | case .notificaiton:
20 | return "notification"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-grid.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icons8-grid-2.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "icons8-grid-1.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "icons8-grid.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-time.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icons8-time.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "icons8-timer.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "icons8-timer-1.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/icons8-trash.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icons8-trash@1x.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "icons8-trash@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "icons8-trash@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/GitBingo/Dependencies/ContributionReceiver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContributionReceiver.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 18/07/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import RxSwift
10 | import RxDataSources
11 |
12 | typealias Receiver = GithubIdReceiver & SectionModelsReceiver
13 |
14 | class ContributionReceiver: Receiver {
15 | var githubID: BehaviorSubject = BehaviorSubject(value: "아이디를 입력해주세요.")
16 | var sectionModels: PublishSubject<[SectionModel]> = PublishSubject()
17 | }
18 |
--------------------------------------------------------------------------------
/GitBingo/Extensions/Storyboarded.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Storyboarded.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 27/06/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol Storyboarded {
12 | static func instantiate() -> Self?
13 | }
14 |
15 | extension Storyboarded where Self: UIViewController {
16 | static func instantiate() -> Self? {
17 | let storyboard = UIStoryboard(name: Self.reusableIdentifier, bundle: nil)
18 | return storyboard.instantiateViewController(withIdentifier: Self.reusableIdentifier) as? Self
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/GitBingo/Extensions/LocalizedString.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocalizedString.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 05/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 | var localized: String {
13 | return NSLocalizedString(self, tableName: "Localizable", value: "\(self)", comment: "")
14 | }
15 |
16 | func localized(with value: String) -> String {
17 | let format = NSLocalizedString(self, tableName: "Localizable", value: "\(self)", comment: "")
18 | return String.localizedStringWithFormat(format, value)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/GitBingoTests/SessionManagerStub.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SessionManagerStub.swift
3 | // GitBingoTests
4 | //
5 | // Created by 이동건 on 29/11/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class SessionManagerStub: SessionManagerProtocol {
12 | private var error: GitBingoError? // 테스트를 위한 에러
13 |
14 | init(_ error: GitBingoError? = nil) {
15 | self.error = error
16 | }
17 |
18 | func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
19 | completionHandler(nil, nil, error) // Call Synchronous
20 | return URLSessionDataTask()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/GitBingoTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/GitBingo/ko.lproj/LaunchScreen.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "UILabel"; text = "Sat"; ObjectID = "0vy-aj-eD6"; */
3 | "0vy-aj-eD6.text" = "Sat";
4 |
5 | /* Class = "UILabel"; text = "Sun"; ObjectID = "6cg-A9-859"; */
6 | "6cg-A9-859.text" = "Sun";
7 |
8 | /* Class = "UILabel"; text = "Wed"; ObjectID = "81g-ML-E5p"; */
9 | "81g-ML-E5p.text" = "Wed";
10 |
11 | /* Class = "UILabel"; text = "Thu"; ObjectID = "DWS-s8-Cy5"; */
12 | "DWS-s8-Cy5.text" = "Thu";
13 |
14 | /* Class = "UILabel"; text = "Tue"; ObjectID = "gke-RR-RM0"; */
15 | "gke-RR-RM0.text" = "Tue";
16 |
17 | /* Class = "UILabel"; text = "Fri"; ObjectID = "j4O-bj-AXn"; */
18 | "j4O-bj-AXn.text" = "Fri";
19 |
20 | /* Class = "UILabel"; text = "GitBingo"; ObjectID = "kSf-IO-C9L"; */
21 | "kSf-IO-C9L.text" = "GitBingo";
22 |
23 | /* Class = "UILabel"; text = "Mon"; ObjectID = "kpW-1R-dlf"; */
24 | "kpW-1R-dlf.text" = "Mon";
25 |
--------------------------------------------------------------------------------
/GitBingo/ViewModel/Home/HomeViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModel.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 26/06/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxDataSources
11 | import RxSwift
12 | import RxCocoa
13 |
14 | typealias ContributionsSectionModel = [SectionModel]
15 |
16 | protocol HomeViewModelType {
17 | var title: Driver { get }
18 | var sections: Driver { get }
19 | }
20 |
21 | class HomeViewModel: HomeViewModelType {
22 | private var receiver: Receiver
23 | var title: Driver { return receiver.githubID.asDriver(onErrorJustReturn: "") }
24 | var sections: Driver { return receiver.sectionModels.asDriver(onErrorJustReturn: [])}
25 |
26 | init(receiver: Receiver) {
27 | self.receiver = receiver
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/GitBingo/Dependencies/ContributionsMapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContributionsMapper.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 29/07/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxDataSources
11 |
12 | protocol ContributionsMapper: class {
13 | associatedtype Source
14 | associatedtype Target
15 |
16 | init()
17 | func mapping(from contributions: Source) -> [Target]
18 | }
19 |
20 | class ContributionsSectionModelMapper: ContributionsMapper {
21 | required init() { }
22 | func mapping(from contributions: Contributions) -> [SectionModel] {
23 | return [SectionModel(model: "This Week", items: contributions.grades.prefix(7).map { $0 }),
24 | SectionModel(model: "Last Weeks", items: contributions.grades.suffix(from: 7).map { $0 })]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/GitBingo/Helper/GitBingoError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GitergyError.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 01/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum GitBingoError: Error {
12 | case pageNotFound
13 | case unexpected
14 | case networkError
15 | case idIsEmpty
16 | case failToRegisterNotification
17 |
18 | var description: String {
19 | switch self {
20 | case .pageNotFound:
21 | return "Invaild ID".localized
22 | case .unexpected:
23 | return "Unexpected Error".localized
24 | case .networkError:
25 | return "Network Error".localized
26 | case .idIsEmpty:
27 | return "Please Input Your Github ID".localized
28 | case .failToRegisterNotification:
29 | return "Registering Notification Failed\nPlease try later".localized
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/GitBingo/Model/Dot.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dot.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 24/08/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class Dot {
12 | // MARK: Properties
13 | private (set) var count: Int?
14 | var isToday: Bool = false
15 | private var date: String?
16 | private var rawColor: String?
17 | var grade: ContributionGrade? {
18 | guard let color = rawColor else { return .notYet }
19 |
20 | return ContributionGrade(rawValue: color)
21 | }
22 | var dateForOrder: Date? {
23 | guard let date = date else { return nil }
24 | let formatter = DateFormatter()
25 | if let date = formatter.date(from: date) {
26 | return date
27 | }
28 |
29 | return nil
30 | }
31 |
32 | // MARK: Life Cycle
33 | init() {}
34 |
35 | init(date: String, color: String, count: Int) {
36 | self.date = date
37 | self.rawColor = color
38 | self.count = count
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/GitBingoTests/MainViewStub.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewStub.swift
3 | // GitBingoTests
4 | //
5 | // Created by 이동건 on 29/11/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class MainViewStub: DotsUpdateableDelegate {
12 | var showProgressStatusDidCalled: Bool = false
13 | var showSuccessProggressStatusDidCalled: Bool = false
14 | var showFailProgressStatusDidCalled: Bool = false
15 | var setUpGithubInputAlertButtonDidCalled: Bool = false
16 |
17 | var error: GitBingoError?
18 |
19 | func showProgressStatus(mode: RefreshMode?) {
20 | showProgressStatusDidCalled = true
21 | }
22 |
23 | func showSuccessProgressStatus() {
24 | showSuccessProggressStatusDidCalled = true
25 | }
26 |
27 | func showFailProgressStatus(with error: GitBingoError) {
28 | self.error = error
29 | showFailProgressStatusDidCalled = true
30 | }
31 |
32 | func setUpGithubInputAlertButton(_ title: String) {
33 | setUpGithubInputAlertButtonDidCalled = true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/GitBingo Widget/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | GitBingo
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | XPC!
19 | CFBundleShortVersionString
20 | 2.0
21 | CFBundleVersion
22 | 2.2
23 | NSExtension
24 |
25 | NSExtensionMainStoryboard
26 | MainInterface
27 | NSExtensionPointIdentifier
28 | com.apple.widget-extension
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/GitBingoTests/APIServiceStub.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIServiceStub.swift
3 | // GitBingoTests
4 | //
5 | // Created by 이동건 on 29/11/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | class APIServiceStub: APIServiceProtocol {
13 | private var session: SessionManagerProtocol
14 | var error: GitBingoError?
15 | var contributions: Contributions?
16 |
17 | init(_ session: SessionManagerProtocol) {
18 | self.session = session
19 | }
20 |
21 | func fetchContributionDots(of id: String, completion: @escaping (Contributions?, GitBingoError?) -> Void) {
22 | guard let url = URL(string: "https://github.com/users/\(id)/contributions") else {
23 | self.error = .pageNotFound
24 | completion(nil, .pageNotFound)
25 | XCTestExpectation(description: "Fail").fulfill()
26 | return
27 | }
28 |
29 | session.dataTask(with: url) { (_, _, error) in
30 | if let error = error {
31 | self.error = error as? GitBingoError
32 | completion(nil, self.error)
33 | return
34 | }
35 |
36 | self.contributions = Contributions(dots: [])
37 | completion(self.contributions, nil)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Differentiator (4.0.1)
3 | - Kanna (4.0.2)
4 | - RxCocoa (5.0.0):
5 | - RxRelay (~> 5)
6 | - RxSwift (~> 5)
7 | - RxDataSources (4.0.1):
8 | - Differentiator (~> 4.0)
9 | - RxCocoa (~> 5.0)
10 | - RxSwift (~> 5.0)
11 | - RxRelay (5.0.0):
12 | - RxSwift (~> 5)
13 | - RxSwift (5.0.0)
14 | - SVProgressHUD (2.2.5)
15 | - SwiftLint (0.28.0)
16 |
17 | DEPENDENCIES:
18 | - Kanna
19 | - RxCocoa
20 | - RxDataSources
21 | - RxSwift
22 | - SVProgressHUD
23 | - SwiftLint
24 |
25 | SPEC REPOS:
26 | https://github.com/cocoapods/specs.git:
27 | - Differentiator
28 | - Kanna
29 | - RxCocoa
30 | - RxDataSources
31 | - RxRelay
32 | - RxSwift
33 | - SVProgressHUD
34 | - SwiftLint
35 |
36 | SPEC CHECKSUMS:
37 | Differentiator: 886080237d9f87f322641dedbc5be257061b0602
38 | Kanna: c60b61d72554bf04ed1ee074f9b379855cab4892
39 | RxCocoa: fcf32050ac00d801f34a7f71d5e8e7f23026dcd8
40 | RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114
41 | RxRelay: 4f7409406a51a55cd88483f21ed898c234d60f18
42 | RxSwift: 8b0671caa829a763bbce7271095859121cbd895f
43 | SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
44 | SwiftLint: 088cfacb75b45970017e62b7524d506776d60148
45 |
46 | PODFILE CHECKSUM: 05e93c40bf5e9501c201c54b0fbd21b711637bd1
47 |
48 | COCOAPODS: 1.5.3
49 |
--------------------------------------------------------------------------------
/GitBingo/Service/Parser/Parser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Parser.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 17/11/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Kanna
10 |
11 | class Parser: HTMLParsingProtocol {
12 | func parse(from data: Data?) -> Contributions? {
13 | guard let data = data, let rawHTML = String(data: data, encoding: .utf8) else { return nil }
14 | guard let doc = try? HTML(html: rawHTML, encoding: .utf8) else { return nil }
15 | if doc.body?.content == "Not Found" { return nil }
16 | let dots = extractDots(from: doc)
17 | return Contributions(dots: dots)
18 | }
19 | private func extractDots(from doc: HTMLDocument) -> [Dot] {
20 | let dayElements = doc.css("g > .day")
21 | let dots = extractDots(from: dayElements)
22 | dots.last?.isToday = true
23 | return dots
24 | }
25 | private func extractDots(from object: XPathObject) -> [Dot] {
26 | var dots: [Dot] = []
27 | dots = object.compactMap { extractDot(from: $0) }
28 | return dots
29 | }
30 | private func extractDot(from element: XMLElement) -> Dot? {
31 | guard let date = element["data-date"] else { return nil }
32 | guard let color = element["fill"] else { return nil }
33 | guard let dataCount = element["data-count"], let count = Int(dataCount) else { return nil }
34 | return Dot(date: date, color: color, count: count)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/GitBingo/Model/ContributionGrade.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContributionGrade.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 24/08/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | enum ContributionGrade: String {
12 | case notYet = "#ffffff"
13 | case noneGreen = "#ebedf0"
14 | case lessGreen = "#c6e48b"
15 | case normalGreen = "#7bc96f"
16 | case hardGreen = "#239a3b"
17 | case extremeGreen = "#196127"
18 |
19 | case lessHalloween = "#feec59"
20 | case normalHalloween = "#fec42e"
21 | case hardHalloween = "#fc9526"
22 | case extremeHalloween = "#03011b"
23 |
24 | var color: UIColor {
25 | switch self {
26 | case .notYet:
27 | return UIColor(hex: 0xffffff)
28 | case .noneGreen:
29 | return UIColor(hex: 0xebedf0)
30 | case .lessGreen:
31 | return UIColor(hex: 0xc6e48b)
32 | case .normalGreen:
33 | return UIColor(hex: 0x7bc96f)
34 | case .hardGreen:
35 | return UIColor(hex: 0x239a3b)
36 | case .extremeGreen:
37 | return UIColor(hex: 0x196127)
38 | case .lessHalloween:
39 | return UIColor(hex: 0xfeec59)
40 | case .normalHalloween:
41 | return UIColor(hex: 0xfec42e)
42 | case .hardHalloween:
43 | return UIColor(hex: 0xfc9526)
44 | case .extremeHalloween:
45 | return UIColor(hex: 0x03011b)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/GitBingo Widget/en.lproj/MainInterface.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "UIButton"; normalTitle = "Tap to register Github ID"; ObjectID = "2oU-Io-qQD"; */
3 | "2oU-Io-qQD.normalTitle" = "Tap to register Github ID";
4 |
5 | /* Class = "UILabel"; text = "Fri"; ObjectID = "AgZ-Rp-itW"; */
6 | "AgZ-Rp-itW.text" = "Fri";
7 |
8 | /* Class = "UILabel"; text = "Sun"; ObjectID = "BiH-oJ-V4Z"; */
9 | "BiH-oJ-V4Z.text" = "Sun";
10 |
11 | /* Class = "UILabel"; text = "-"; ObjectID = "GUl-cw-IYT"; */
12 | "GUl-cw-IYT.text" = "-";
13 |
14 | /* Class = "UILabel"; text = "-"; ObjectID = "Ik4-qu-RBD"; */
15 | "Ik4-qu-RBD.text" = "-";
16 |
17 | /* Class = "UILabel"; text = "Thu"; ObjectID = "QPl-Cs-wKo"; */
18 | "QPl-Cs-wKo.text" = "Thu";
19 |
20 | /* Class = "UILabel"; text = "Week"; ObjectID = "Uie-Lr-aui"; */
21 | "Uie-Lr-aui.text" = "Week";
22 |
23 | /* Class = "UILabel"; text = "Wed"; ObjectID = "VPc-Ns-rz3"; */
24 | "VPc-Ns-rz3.text" = "Wed";
25 |
26 | /* Class = "UILabel"; text = "Today"; ObjectID = "bgu-9G-zLB"; */
27 | "bgu-9G-zLB.text" = "Today";
28 |
29 | /* Class = "UILabel"; text = "Notification"; ObjectID = "fvE-km-Vtg"; */
30 | "fvE-km-Vtg.text" = "Notification";
31 |
32 | /* Class = "UILabel"; text = "-"; ObjectID = "gBG-lO-kj0"; */
33 | "gBG-lO-kj0.text" = "-";
34 |
35 | /* Class = "UILabel"; text = "Sat"; ObjectID = "jRr-gW-BWt"; */
36 | "jRr-gW-BWt.text" = "Sat";
37 |
38 | /* Class = "UILabel"; text = "Tue"; ObjectID = "yx3-bE-QBm"; */
39 | "yx3-bE-QBm.text" = "Tue";
40 |
41 | /* Class = "UILabel"; text = "Mon"; ObjectID = "zTf-Ux-BFK"; */
42 | "zTf-Ux-BFK.text" = "Mon";
43 |
--------------------------------------------------------------------------------
/GitBingo Widget/ko.lproj/MainInterface.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "UIButton"; normalTitle = "Tap to register Github ID"; ObjectID = "2oU-Io-qQD"; */
3 | "2oU-Io-qQD.normalTitle" = "Tap to register Github ID";
4 |
5 | /* Class = "UILabel"; text = "Fri"; ObjectID = "AgZ-Rp-itW"; */
6 | "AgZ-Rp-itW.text" = "Fri";
7 |
8 | /* Class = "UILabel"; text = "Sun"; ObjectID = "BiH-oJ-V4Z"; */
9 | "BiH-oJ-V4Z.text" = "Sun";
10 |
11 | /* Class = "UILabel"; text = "-"; ObjectID = "GUl-cw-IYT"; */
12 | "GUl-cw-IYT.text" = "-";
13 |
14 | /* Class = "UILabel"; text = "-"; ObjectID = "Ik4-qu-RBD"; */
15 | "Ik4-qu-RBD.text" = "-";
16 |
17 | /* Class = "UILabel"; text = "Thu"; ObjectID = "QPl-Cs-wKo"; */
18 | "QPl-Cs-wKo.text" = "Thu";
19 |
20 | /* Class = "UILabel"; text = "Week"; ObjectID = "Uie-Lr-aui"; */
21 | "Uie-Lr-aui.text" = "Week";
22 |
23 | /* Class = "UILabel"; text = "Wed"; ObjectID = "VPc-Ns-rz3"; */
24 | "VPc-Ns-rz3.text" = "Wed";
25 |
26 | /* Class = "UILabel"; text = "Today"; ObjectID = "bgu-9G-zLB"; */
27 | "bgu-9G-zLB.text" = "Today";
28 |
29 | /* Class = "UILabel"; text = "Notification"; ObjectID = "fvE-km-Vtg"; */
30 | "fvE-km-Vtg.text" = "Notification";
31 |
32 | /* Class = "UILabel"; text = "-"; ObjectID = "gBG-lO-kj0"; */
33 | "gBG-lO-kj0.text" = "-";
34 |
35 | /* Class = "UILabel"; text = "Sat"; ObjectID = "jRr-gW-BWt"; */
36 | "jRr-gW-BWt.text" = "Sat";
37 |
38 | /* Class = "UILabel"; text = "Tue"; ObjectID = "yx3-bE-QBm"; */
39 | "yx3-bE-QBm.text" = "Tue";
40 |
41 | /* Class = "UILabel"; text = "Mon"; ObjectID = "zTf-Ux-BFK"; */
42 | "zTf-Ux-BFK.text" = "Mon";
43 |
--------------------------------------------------------------------------------
/GitBingo/Helper/GitBingoAlertState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GitBingoAlert.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 22/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | enum GitBingoAlertState {
12 | case register(Bool, String, (UIAlertAction)->Void)
13 | case unauthorized
14 | case registerFailed
15 | case removeNotification(((UIAlertAction)->Void)?)
16 |
17 | var alert: UIAlertController {
18 | switch self {
19 | case .register(let hasScheduledNotification, let time, let handler):
20 | if hasScheduledNotification {
21 | return UIAlertController.getAlert(title: "🤓", message: "Scheduled Notification Existed.\nDo you want to UPDATE it to %@?".localized(with: time), with: handler)
22 | }
23 |
24 | return UIAlertController.getAlert(title: "Register".localized, message: "Do you want to GET Notification at\n%@ daily?".localized(with: time), with: handler)
25 | case .unauthorized:
26 | return UIAlertController.getAlert(title: "Not Authorized".localized, message: "CHECK Notifications Configuration in Settings".localized)
27 | case .registerFailed:
28 | return UIAlertController.getAlert(title: "Error".localized, message: GitBingoError.failToRegisterNotification.description)
29 | case .removeNotification(let handler):
30 | return UIAlertController.getAlert(title: "Remove".localized, message: "Do you really want to REMOVE Scheduled Notification?".localized, with: handler)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/GitBingo/Localizing/ko.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | GitBingo
4 |
5 | Created by 이동건 on 05/09/2018.
6 | Copyright © 2018 이동건. All rights reserved.
7 | */
8 |
9 | // AppDelegate.swift
10 | "Set Alarm" = "알람 설정";
11 |
12 | // RegisterAlertViewController.swift
13 | "Scheduled Notification Existed.\nDo you want to UPDATE it to %@?" = "이미 등록된 알람이 존재합니다.\n%@로 새로운 알람을 등록하시겠습니까?";
14 | "Register" = "등록";
15 | "Do you want to GET Notification at\n%@ daily?" = "매일 %@에 알람을 받으시겠습니까?";
16 | "Not Authorized" = "권한 없음";
17 | "CHECK Notifications Configuration in Settings" = "시스템 설정에서 앱의 알람 설정을 확인하세요.";
18 | "Error" = "에러";
19 | "Remove" = "삭제";
20 | "Do you really want to REMOVE Scheduled Notification?" = "예약된 알람을 삭제하시겠습니까?";
21 |
22 | // GitBingoError.swift
23 | "Invaild ID" = "존재하지 않는 아이디입니다.";
24 | "Unexpected Error" = "예상치 못한 에러가 발생했습니다.";
25 | "Network Error" = "네트워크 에러";
26 | "Please Input Your Github ID" = "Github 아이디를 입력해주세요.";
27 | "Registering Notification Failed\nPlease try later" = "알람 등록에 실패했습니다.\n잠시 후 시도해주세요.";
28 |
29 | // RegisterViewPresenter.swift
30 | "No Scheduled Notification so far" = "등록된 알람이 없습니다.";
31 | "Scheduled at %@" = "%@에 예약된 알람이 있습니다.";
32 | "Wait!" = "잠깐!";
33 | "Did You Commit?🤔" = "커밋은 하셨나요?🤔";
34 |
35 | // CustomAlert.swift
36 | "Input your Github ID" = "Github 아이디를 입력하세요.";
37 | "Welcome! %@👋"="안녕하세요! %@님👋";
38 | "Done" = "완료";
39 | "Cancel" = "취소";
40 | "Ok" = "확인";
41 | "Yes" = "예";
42 | "No" = "아니오";
43 |
44 | // GitBingo Widget MainInterface.storyboard
45 | "Today" = "오늘";
46 | "Week" = "이번 주";
47 | "Notification" = "예약된 알람";
48 |
--------------------------------------------------------------------------------
/GitBingo/Service/GroupUserDefaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GroupUserDefaults.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 27/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol GitbingoStorage {
12 | func save(_ data: T, of type: KeyIdentifier)
13 | func load(of type: KeyIdentifier) -> T?
14 | func remove(of type: KeyIdentifier)
15 | }
16 |
17 | class UserProfileStorage: GitbingoStorage {
18 | private var userDefaults: UserDefaults?
19 |
20 | init(_ suiteName: String) {
21 | self.userDefaults = UserDefaults(suiteName: suiteName)
22 | }
23 |
24 | func save(_ data: T, of type: KeyIdentifier) {
25 | print(data)
26 | userDefaults?.set(data, forKey: type.value)
27 | }
28 |
29 | func load(of type: KeyIdentifier) -> T? {
30 | return userDefaults?.value(forKey: type.value) as? T
31 | }
32 |
33 | func remove(of type: KeyIdentifier) {
34 | userDefaults?.removeObject(forKey: type.value)
35 | }
36 | }
37 |
38 | class GroupUserDefaults {
39 | static let shared = GroupUserDefaults()
40 | private var groupUserDefaults = UserDefaults(suiteName: "group.Gitbingo")!
41 | private init() {}
42 |
43 | func save(_ data: String, of type: KeyIdentifier) {
44 | groupUserDefaults.set(data, forKey: type.value)
45 | }
46 |
47 | func load(of type: KeyIdentifier) -> Any? {
48 | return groupUserDefaults.value(forKey: type.value)
49 | }
50 |
51 | func remove(of type: KeyIdentifier) {
52 | groupUserDefaults.removeObject(forKey: type.value)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/GitBingo/Model/Contributions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Contributions.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 24/08/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class Contributions {
12 | // MARK: Properties
13 | var dots: [Dot]
14 | var count: Int {
15 | return dots.count
16 | }
17 | var grades: [ContributionGrade] {
18 | return dots.compactMap { $0.grade }
19 | }
20 | var colors: [UIColor?] {
21 | let colors: [UIColor?] = dots.map {$0.grade?.color}
22 |
23 | return colors
24 | }
25 | var total: Int {
26 | var total: Int = 0
27 | dots.forEach {
28 | total += $0.count ?? 0
29 | }
30 | return total
31 | }
32 | var today: Int {
33 | let today = dots.filter {$0.isToday == true}
34 | return today.first?.count ?? 0
35 | }
36 |
37 | // MARK: Life Cycle
38 | init(dots: [Dot]) {
39 | var dots = dots
40 |
41 | let thisWeekContributedDate = dots.count % 7
42 | let notYetDot = 7 - thisWeekContributedDate
43 |
44 | if notYetDot != 7 {
45 | for _ in (0..
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | GitBingo
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 2.0
21 | CFBundleURLTypes
22 |
23 |
24 | CFBundleTypeRole
25 | Editor
26 | CFBundleURLName
27 | com.dk.GitBingo.Host
28 | CFBundleURLSchemes
29 |
30 | GitBingoHost
31 |
32 |
33 |
34 | CFBundleVersion
35 | 2.2
36 | LSRequiresIPhoneOS
37 |
38 | NSAppTransportSecurity
39 |
40 | NSAllowsArbitraryLoads
41 |
42 |
43 | UILaunchStoryboardName
44 | LaunchScreen
45 | UIRequiredDeviceCapabilities
46 |
47 | armv7
48 |
49 | UISupportedInterfaceOrientations
50 |
51 | UIInterfaceOrientationPortrait
52 |
53 | UISupportedInterfaceOrientations~ipad
54 |
55 | UIInterfaceOrientationPortrait
56 | UIInterfaceOrientationPortraitUpsideDown
57 | UIInterfaceOrientationLandscapeLeft
58 | UIInterfaceOrientationLandscapeRight
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/GitBingo/ViewControllers/RegisterAlert/RegisterAlertViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegisterAlertViewController.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 02/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class RegisterAlertViewController: UIViewController {
12 | // MARK: Outlets
13 | @IBOutlet weak var scheduledNotificationIndicator: UILabel!
14 | @IBOutlet weak var timePicker: UIDatePicker! {
15 | didSet {
16 | timePicker.date = Date()
17 | }
18 | }
19 |
20 | // MARK: Properties
21 | private var presenter: RegisterViewPresenter = RegisterViewPresenter()
22 |
23 | // MARK: Life Cycle
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 | presenter.attachView(self)
27 | presenter.updateScheduledNotificationIndicator()
28 | }
29 |
30 | deinit {
31 | presenter.detatchView()
32 | }
33 |
34 | // MARK: Actions
35 | @IBAction func handleRegister(_ sender: UIButton) {
36 | presenter.showAlert()
37 | }
38 |
39 | @IBAction func valueDidChanged(_ sender: UIDatePicker) {
40 | presenter.setupTime(with: sender.date)
41 | }
42 |
43 | @IBAction func handleRemoveNotification(_ sender: UIBarButtonItem) {
44 | presenter.removeNotification()
45 | }
46 |
47 | @IBAction func handleDone(_ sender: UIBarButtonItem) {
48 | self.dismiss(animated: true, completion: nil)
49 | }
50 | }
51 |
52 | // MARK: - RegisterNotificationProtocol
53 | extension RegisterAlertViewController: RegisterNotificationProtocol {
54 | func dismissVC() {
55 | self.dismiss(animated: true, completion: nil)
56 | }
57 |
58 | func showAlert(alertState: GitBingoAlertState) {
59 | let alert = alertState.alert
60 |
61 | present(alert, animated: true, completion: nil)
62 | }
63 |
64 | func updateDescriptionLabel(with text: String) {
65 | self.scheduledNotificationIndicator.text = text
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/GitBingo/Dependencies/HomeViewDependencyContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewDependencyContainer.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 18/07/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 | class GitbingoAppDependencyContainer {
13 | private let userProfileStorage: GitbingoStorage
14 |
15 | init(_ suiteName: String) {
16 | self.userProfileStorage = UserProfileStorage(suiteName)
17 | }
18 |
19 | private func generateReceiver() -> Receiver {
20 | return ContributionReceiver()
21 | }
22 |
23 | func generateHomeViewController() -> HomeViewController? {
24 | return HomeViewController.instantiate(with: generateHomeViewDependencyContainer())
25 | }
26 |
27 | func generateHomeViewDependencyContainer() -> HomeViewDependencyContainer {
28 | return HomeViewDependencyContainer(receiver: generateReceiver(), userProfileStorage)
29 | }
30 | }
31 |
32 | class HomeViewDependencyContainer {
33 | private let receiver: Receiver
34 | private let storage: GitbingoStorage
35 | private let disposeBag = DisposeBag()
36 |
37 | init(receiver: Receiver, _ storage: GitbingoStorage) {
38 | self.receiver = receiver
39 | self.storage = storage
40 | bind()
41 | }
42 |
43 | private func bind() {
44 | receiver.githubID.skip(1).subscribe(onNext: { [weak self] in self?.storage.save($0, of: .id) }).disposed(by: disposeBag)
45 | }
46 |
47 | func generateHomeViewModel() -> HomeViewModelType {
48 | return HomeViewModel(receiver: receiver)
49 | }
50 |
51 | func generateIDInputViewModel() -> IDInputViewModelType {
52 | return IDInputViewModel(contributionsDotsRepository: generateContributionDotsRepository(), receiver: receiver, mapper: ContributionsSectionModelMapper())
53 | }
54 |
55 | private func generateContributionDotsRepository() -> ContributionDotsRepository {
56 | return GitBingoContributionDotsRepository(parser: Parser(), session: URLSession(configuration: .default))
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/GitBingo/Extensions/CustomAlert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomAlert.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 03/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIAlertController {
12 |
13 | static func getTextFieldAlert(_ completion: @escaping (String) -> Void ) -> UIAlertController {
14 | let alert = UIAlertController(title: "Github ID", message: nil, preferredStyle: .alert)
15 |
16 | alert.addTextField { (textField) in
17 | textField.placeholder = "Input your Github ID".localized
18 | }
19 |
20 | alert.addAction(UIAlertAction(title: "Done".localized, style: .default, handler: { (_) in
21 | guard let id = alert.textFields?[0].text else { return }
22 | completion(id)
23 | }))
24 |
25 | alert.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil))
26 |
27 | alert.actions.first?.isEnabled = false
28 |
29 | return alert
30 | }
31 |
32 | static func getAlert(title: String, message: String) -> UIAlertController {
33 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
34 |
35 | alert.addAction(UIAlertAction(title: "Ok".localized, style: .default, handler: nil))
36 |
37 | return alert
38 | }
39 |
40 | static func getAlert(title: String, message: String, with completion: ((UIAlertAction) -> Void)?) -> UIAlertController {
41 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
42 |
43 | alert.addAction(UIAlertAction(title: "Yes".localized, style: .default, handler: completion))
44 | alert.addAction(UIAlertAction(title: "No".localized, style: .cancel, handler: nil))
45 |
46 | return alert
47 | }
48 |
49 | @objc func handleEdtingChanged(_ textField: UITextField) {
50 | guard let text = textField.text else { return }
51 | guard text.isEmpty else {
52 | actions.first?.isEnabled = true
53 | return
54 | }
55 |
56 | actions.first?.isEnabled = false
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## [GitBingo (깃빙고)](https://itunes.apple.com/kr/app/gitbingo/id1435428800?l=en&mt=8)
2 |
3 | > App Icon by [snowJang24](https://github.com/snowjang24), App Name by [nailerHeum](https://github.com/nailerHeum)
4 |
5 | **1일 1커밋을 실천하려는 개발자들을 위한 어플리케이션**
6 |
7 | ### 기능
8 |
9 | 1. 실시간 Contribution 확인 가능
10 | 2. 원하는 시간에 알람을 받아 금일 커밋을 진행하였는지 확인 가능
11 | 3. 투데이 익스텐션 타겟을 사용해 위젯에서 이번주 커밋 정보와 알람 등록 시간을 확인 가능
12 |
13 | ### 사용한 기술
14 |
15 | - `Swift4`, `Xcode9`, `UserNotifications`, `Error Handling`, `Localizing`, `Networking`, `UIApplicationShortCuts`, `Today Extensions`, `Unit Test`, `SwiftLint`
16 |
17 | ### 사용한 아키텍쳐
18 |
19 | - `Delegation `, `Singleton`
20 | - 시도한 아키텍쳐 : `MVP`
21 |
22 | ### 공부한 내용 정리
23 |
24 | - App Extension Programming Guide for iOS
25 | - [Essential](https://ehdrjsdlzzzz.github.io/2018/10/03/App-Extension-Programming-Guide-1/)
26 | - [Essential - Handling Common Scenarios](https://ehdrjsdlzzzz.github.io/2018/10/09/App-Extension-Programming-Guide-2/)
27 |
28 | ### 사용한 라이브러리
29 |
30 | - [`Kanna`](https://github.com/tid-kijyun/Kanna) - https://github.com/users/ehdrjsdlzzzz/contributions 로부터 HTML을 파싱해오기 위해 사용
31 | - [`SVProgressHUD`](https://github.com/SVProgressHUD/SVProgressHUD) - Indicator와 함께 코멘트를 사용하기 위해 사용
32 | - [`SwiftLint`](https://github.com/realm/SwiftLint) - Swift 스타일과 컨벤션을 지키기 위해
33 |
34 | ### 문제점
35 |
36 | - 웹 브라우저에선 00:00 이후 다음 날 컨트리뷰션 도트를 바로 확인 가능하지만 앱에선 GMT 시간 차로 오전 9시가 되어서야 그날의 도트를 받아올 수 있음.
37 | - 이 문제를 해결하기 위해 많은 노력을 해보았으나 해결하지 못함
38 | - 확인해본 결과 모바일 크롬이나 사파리에서도 동일한 이슈가 발생한 것으로 모바일에서 요청한 것에 대한 깃헙 서버의 응답에 이슈가 있는 것으로 판단하였음.
39 |
40 | ### 다이어그램
41 |
42 | **GitBingo**
43 |
44 | 
45 |
46 | **Today Extension**
47 |
48 | 
49 |
50 | #### 스크린샷
51 |
52 | **메인**
53 |
54 | 
55 |
56 | **알람 설정**
57 |
58 | 
59 |
60 | **알람 수신**
61 |
62 | > 알람 수신을 스크린샷을 위해 위의 알람 설정 화면에서 설정한 시간과 차이가 존재합니다.
63 |
64 | 
65 |
66 | **Version 2.0 - Widget**
67 |
68 | 
69 |
--------------------------------------------------------------------------------
/GitBingo/Presenters/MainViewPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewPresenter.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 31/08/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol DotsUpdateableDelegate: class {
12 | func showProgressStatus(mode: RefreshMode?)
13 | func showSuccessProgressStatus()
14 | func showFailProgressStatus(with error: GitBingoError)
15 | func setUpGithubInputAlertButton(_ title: String)
16 | }
17 |
18 | class MainViewPresenter {
19 | // MARK: Properties
20 | private weak var vc: DotsUpdateableDelegate?
21 | private var contributions: Contributions?
22 | private var service: APIServiceProtocol
23 | var dotsCount: Int {
24 | return contributions?.count ?? 0
25 | }
26 | private var id: String? {
27 | return GroupUserDefaults.shared.load(of: .id) as? String
28 | }
29 | private var greeting: String {
30 | guard let id = self.id else { return "Hello, Who are you?" }
31 | return "Welcome! \(id)👋"
32 | }
33 |
34 | // MARK: Life Cycle
35 | init(service: APIServiceProtocol) {
36 | self.service = service
37 | }
38 |
39 | func attachView(_ vc: DotsUpdateableDelegate) {
40 | self.vc = vc
41 | }
42 |
43 | func detatchView() {
44 | self.vc = nil
45 | }
46 |
47 | func refresh(mode: RefreshMode) {
48 | guard let id = self.id else { return }
49 | request(from: id, mode: mode)
50 | }
51 |
52 | func request(from id: String? = nil, mode: RefreshMode? = nil) {
53 | if let id = id ?? self.id {
54 | self.vc?.showProgressStatus(mode: mode)
55 | service.fetchContributionDots(of: id) { (contributions, err) in
56 | if let err = err {
57 | self.vc?.showFailProgressStatus(with: err)
58 | return
59 | }
60 |
61 | self.contributions = contributions
62 | self.vc?.showSuccessProgressStatus()
63 | self.vc?.setUpGithubInputAlertButton(self.greeting)
64 |
65 | GroupUserDefaults.shared.save(id, of: .id)
66 | }
67 | return
68 | }
69 | vc?.setUpGithubInputAlertButton(greeting)
70 | }
71 |
72 | func color(at item: Int) -> UIColor? {
73 | return contributions?.colors[item]
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/GitBingo/Service/APIService/APIService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIService.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 23/08/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 | import RxCocoa
12 | import RxDataSources
13 |
14 | protocol APIServiceProtocol: class {
15 | func fetchContributionDots(of id: String, completion: @escaping (Contributions?, GitBingoError?) -> Void)
16 | }
17 |
18 | protocol ContributionDotsRepository {
19 | func fetch(_ id: String) -> Observable
20 | }
21 |
22 | class GitBingoContributionDotsRepository: ContributionDotsRepository {
23 | private let session: SessionManagerProtocol
24 | private let parser: HTMLParsingProtocol
25 |
26 | init(parser: HTMLParsingProtocol, session: SessionManagerProtocol) {
27 | self.parser = parser
28 | self.session = session
29 | }
30 |
31 | func fetch(_ id: String) -> Observable {
32 | let url = URL(string: "https://github.com/users/\(id)/contributions")!
33 | return URLSession.shared.rx.response(request: URLRequest(url: url)).map { response, data in
34 | print(response.statusCode)
35 | if 200..<300 ~= response.statusCode {
36 | guard let contributions = self.parser.parse(from: data) else { throw GitBingoError.pageNotFound }
37 | return contributions
38 | }
39 | throw GitBingoError.pageNotFound
40 | }
41 | }
42 | }
43 |
44 | class APIService: APIServiceProtocol {
45 | // MARK: Properties
46 | private let session: SessionManagerProtocol
47 | private let parser: HTMLParsingProtocol
48 |
49 | init(parser: HTMLParsingProtocol, session: SessionManagerProtocol) {
50 | self.parser = parser
51 | self.session = session
52 | }
53 |
54 | // MARK: Methods
55 | func fetchContributionDots(of id: String, completion: @escaping (Contributions?, GitBingoError?) -> Void) {
56 | guard let url = URL(string: "https://github.com/users/\(id)/contributions") else {
57 | completion(nil, .pageNotFound)
58 | return
59 | }
60 | let task = self.session.dataTask(with: url) { (data, _, error) in
61 | if error != nil {
62 | completion(nil, .networkError)
63 | return
64 | }
65 |
66 | if let contributions = self.parser.parse(from: data) {
67 | completion(contributions, nil)
68 | } else {
69 | completion(nil, .pageNotFound)
70 | }
71 | }
72 |
73 | task.resume()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/GitBingo/ViewModel/IDInputVIewModel/IDInputViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IDInputViewModel.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 29/06/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 | import RxCocoa
12 | import RxDataSources
13 |
14 | enum ResponseStatus {
15 | case success
16 | case failed(Error)
17 | }
18 |
19 | protocol IDInputViewModelType {
20 | var inputText: BehaviorSubject { get }
21 | var isLoading: PublishSubject { get }
22 | var responseStatus: PublishSubject { get }
23 | var doneButtonValidation: BehaviorSubject { get }
24 | func fetch()
25 | }
26 |
27 | class IDInputViewModel: IDInputViewModelType where M.Source == Contributions, M.Target == SectionModel {
28 | var inputText: BehaviorSubject = BehaviorSubject(value: "")
29 | var isLoading: PublishSubject = PublishSubject()
30 | var responseStatus: PublishSubject = PublishSubject()
31 | var doneButtonValidation: BehaviorSubject = BehaviorSubject(value: false)
32 |
33 | private var contributionsDotsRepository: ContributionDotsRepository
34 | private var receiver: Receiver
35 | private var mapper: M
36 | private var id: BehaviorRelay = BehaviorRelay(value: "")
37 | private var disposeBag = DisposeBag()
38 |
39 | init(contributionsDotsRepository: ContributionDotsRepository, receiver: Receiver, mapper: M) {
40 | self.contributionsDotsRepository = contributionsDotsRepository
41 | self.receiver = receiver
42 | self.mapper = mapper
43 | bind()
44 | }
45 |
46 | func fetch() {
47 | isLoading.onNext(true)
48 | let input = id.value
49 | contributionsDotsRepository
50 | .fetch(input)
51 | .map { self.mapper.mapping(from: $0)}
52 | .subscribe(onNext: { sectionModels in
53 | self.isLoading.onNext(false)
54 | self.responseStatus.onNext(.success)
55 | self.receiver.githubID.onNext(input)
56 | self.receiver.sectionModels.onNext(sectionModels)
57 | }, onError: { error in
58 | self.isLoading.onNext(false)
59 | self.responseStatus.onNext(.failed(error))
60 | })
61 | .disposed(by: disposeBag)
62 | }
63 |
64 | private func bind() {
65 | bindIDTextField()
66 | }
67 |
68 | private func bindIDTextField() {
69 | inputText.bind(to: id).disposed(by: disposeBag)
70 | inputText.map { !$0.isEmpty }.bind(to: doneButtonValidation).disposed(by: disposeBag)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/GitBingoTests/GitBingoPresenterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GitBingoPresenterTests.swift
3 | // GitBingoTests
4 | //
5 | // Created by 이동건 on 29/11/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class GitBingoPresenterTests: XCTestCase {
12 | private var session: SessionManagerStub!
13 | private var apiService: APIServiceStub!
14 | private var mainView: MainViewStub!
15 | private var presenter: MainViewPresenter!
16 |
17 | func testPresenterAction_with_InvalidID() {
18 | givenASession()
19 | givenAAPIService()
20 | givenAMainView()
21 | givenAPresenter()
22 |
23 | whenPresenterRequest(with: "ㅋㅋㅋ")
24 |
25 | thenMainViewShowProgressStatus()
26 | thenMainViewShowFailProgress(with: .pageNotFound)
27 | }
28 |
29 | func testPresenterAction_with_ValidID() {
30 | givenASession()
31 | givenAAPIService()
32 | givenAMainView()
33 | givenAPresenter()
34 |
35 | whenPresenterRequest(with: "ehdrjsdlzzzz")
36 |
37 | thenMainViewShowProgressStatus()
38 | thenMainViewShowSuccessProgressStatus()
39 | }
40 |
41 | func testPresenterAction_with_NetworkError() {
42 | givenASession(with: .networkError)
43 | givenAAPIService()
44 | givenAMainView()
45 | givenAPresenter()
46 |
47 | whenPresenterRequest(with: "ehdrjsdlzzzz")
48 |
49 | thenMainViewShowProgressStatus()
50 | thenMainViewShowFailProgress(with: .networkError)
51 | }
52 |
53 | private func givenASession(with error: GitBingoError? = nil) {
54 | session = SessionManagerStub(error)
55 | }
56 |
57 | private func givenAAPIService() {
58 | apiService = APIServiceStub(session)
59 | }
60 |
61 | private func givenAMainView() {
62 | mainView = MainViewStub()
63 | }
64 |
65 | private func givenAPresenter() {
66 | presenter = MainViewPresenter(service: apiService)
67 | presenter.attachView(mainView)
68 | }
69 |
70 | private func whenPresenterRequest(with id: String) {
71 | presenter.request(from: id)
72 | }
73 |
74 | private func thenMainViewShowProgressStatus() {
75 | XCTAssertTrue(mainView.showProgressStatusDidCalled)
76 | }
77 |
78 | private func thenMainViewShowFailProgress(with error: GitBingoError) {
79 | XCTAssertNotNil(mainView.error)
80 | XCTAssertEqual(mainView.error!, error)
81 | XCTAssertTrue(mainView.showFailProgressStatusDidCalled)
82 | }
83 |
84 | private func thenMainViewShowSuccessProgressStatus() {
85 | XCTAssertNil(mainView.error)
86 | XCTAssertTrue(mainView.showSuccessProggressStatusDidCalled)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/GitBingo Widget/TodayViewPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodayViewPresenter.swift
3 | // GitBingo Widget
4 | //
5 | // Created by 이동건 on 02/10/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // MARK: Protocol
12 | protocol GitBingoWidgetProtocol: class {
13 | func startLoad()
14 | func endLoad()
15 | func hide(error: GitBingoError?)
16 | func initUI(with contributions: Contributions?, at time: String)
17 | func open(_ url: URL)
18 | }
19 |
20 | class TodayViewPresenter {
21 | // MARK: Properties
22 | private weak var vc: GitBingoWidgetProtocol?
23 | private var contributions: Contributions?
24 | private var service: APIServiceProtocol?
25 |
26 | // MARK: Life Cycle
27 | init(service: APIServiceProtocol) {
28 | self.service = service
29 | }
30 |
31 | func attachView(_ vc: GitBingoWidgetProtocol) {
32 | self.vc = vc
33 | }
34 |
35 | func detachView() {
36 | self.vc = nil
37 | }
38 |
39 | func colors(at indexPath: IndexPath) -> UIColor? {
40 | return contributions?.colors[indexPath.item]
41 | }
42 |
43 | func load() {
44 | guard let id = GroupUserDefaults.shared.load(of: .id) as? String else {
45 | vc?.hide(error: .idIsEmpty)
46 | return
47 | }
48 |
49 | if let reserverdNotificaitonTime = GroupUserDefaults.shared.load(of: .notification) as? String {
50 | fetch(of: id, at: reserverdNotificaitonTime)
51 | return
52 | }
53 |
54 | fetch(of: id, at: "➕")
55 | }
56 |
57 | func handleUserInteraction(type: AppURL) {
58 | guard let url = URL(string: "GitBingoHost://\(type.path)") else { return }
59 | vc?.open(url)
60 | }
61 |
62 | private func fetch(of id: String, at time: String) {
63 | vc?.startLoad()
64 | fetchContributions(of: id) { [weak self] (err) in
65 | if let err = err {
66 | self?.vc?.hide(error: err)
67 | self?.vc?.endLoad()
68 | return
69 | }
70 | self?.vc?.hide(error: nil)
71 | self?.vc?.initUI(with: self?.contributions, at: time)
72 | self?.vc?.endLoad()
73 | }
74 | }
75 |
76 | private func fetchContributions(of id: String, completion: @escaping (_ error: GitBingoError?) -> Void ) {
77 | service?.fetchContributionDots(of: id) { [weak self] (contributions, err) in
78 | if let err = err {
79 | completion(err)
80 | return
81 | }
82 |
83 | DispatchQueue.main.async {
84 | guard let dots = contributions?.dots else { return }
85 | let thisWeekContributions = Contributions(dots: dots.prefix(7).map {$0})
86 | self?.contributions = thisWeekContributions
87 | completion(nil)
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/xcode,macos,swift,cocoapods
3 |
4 | ### CocoaPods ###
5 | ## CocoaPods GitIgnore Template
6 |
7 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing
8 | # - Also handy if you have a large number of dependant pods
9 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE
10 | Pods/
11 |
12 | ### macOS ###
13 | # General
14 | .DS_Store
15 | .AppleDouble
16 | .LSOverride
17 |
18 | # Icon must end with two \r
19 | Icon
20 |
21 | # Thumbnails
22 | ._*
23 |
24 | # Files that might appear in the root of a volume
25 | .DocumentRevisions-V100
26 | .fseventsd
27 | .Spotlight-V100
28 | .TemporaryItems
29 | .Trashes
30 | .VolumeIcon.icns
31 | .com.apple.timemachine.donotpresent
32 |
33 | # Directories potentially created on remote AFP share
34 | .AppleDB
35 | .AppleDesktop
36 | Network Trash Folder
37 | Temporary Items
38 | .apdisk
39 |
40 | ### Swift ###
41 | # Xcode
42 | #
43 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
44 |
45 | ## Build generated
46 | build/
47 | DerivedData/
48 |
49 | ## Various settings
50 | *.pbxuser
51 | !default.pbxuser
52 | *.mode1v3
53 | !default.mode1v3
54 | *.mode2v3
55 | !default.mode2v3
56 | *.perspectivev3
57 | !default.perspectivev3
58 | xcuserdata/
59 |
60 | ## Other
61 | *.moved-aside
62 | *.xccheckout
63 | *.xcscmblueprint
64 |
65 | ## Obj-C/Swift specific
66 | *.hmap
67 | *.ipa
68 | *.dSYM.zip
69 | *.dSYM
70 |
71 | ## Playgrounds
72 | timeline.xctimeline
73 | playground.xcworkspace
74 |
75 | # Swift Package Manager
76 | #
77 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
78 | # Packages/
79 | # Package.pins
80 | # Package.resolved
81 | .build/
82 |
83 | # CocoaPods
84 | #
85 | # We recommend against adding the Pods directory to your .gitignore. However
86 | # you should judge for yourself, the pros and cons are mentioned at:
87 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
88 | #
89 | # Pods/
90 | #
91 | # Add this line if you want to avoid checking in source code from the Xcode workspace
92 | # *.xcworkspace
93 |
94 | # Carthage
95 | #
96 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
97 | # Carthage/Checkouts
98 |
99 | Carthage/Build
100 |
101 | # fastlane
102 | #
103 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
104 | # screenshots whenever they are needed.
105 | # For more information about the recommended setup visit:
106 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
107 |
108 | fastlane/report.xml
109 | fastlane/Preview.html
110 | fastlane/screenshots/**/*.png
111 | fastlane/test_output
112 |
113 | ### Xcode ###
114 | # Xcode
115 | #
116 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
117 |
118 | ## User settings
119 |
120 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
121 |
122 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
123 |
124 | ### Xcode Patch ###
125 | *.xcodeproj/*
126 | !*.xcodeproj/project.pbxproj
127 | !*.xcodeproj/xcshareddata/
128 | !*.xcworkspace/contents.xcworkspacedata
129 | /*.gcno
130 |
131 |
132 | # End of https://www.gitignore.io/api/xcode,macos,swift,cocoapods
133 |
--------------------------------------------------------------------------------
/GitBingo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 23/08/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import UserNotifications
11 |
12 | @UIApplicationMain
13 | class AppDelegate: UIResponder, UIApplicationDelegate {
14 |
15 | var window: UIWindow?
16 | private var appDependencyContainer = GitbingoAppDependencyContainer("group.Gitbingo")
17 |
18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
19 | setupNavigationBarAppearance()
20 | setupUserNotification()
21 | setupWindow()
22 | addShortcuts(to: application)
23 | return true
24 | }
25 |
26 | private func setupNavigationBarAppearance() {
27 | UINavigationBar.appearance().tintColor = .black
28 | UINavigationBar.appearance().backgroundColor = .white
29 | UINavigationBar.appearance().isTranslucent = false
30 | UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black,
31 | NSAttributedString.Key.font: UIFont(name: "Apple Color Emoji", size: 21)!]
32 | }
33 |
34 | private func setupUserNotification() {
35 | let center = UNUserNotificationCenter.current()
36 | let options: UNAuthorizationOptions = [.alert, .sound, .badge]
37 | center.requestAuthorization(options: options) { (_, _) in }
38 | }
39 |
40 | private func setupWindow() {
41 | window = UIWindow()
42 | window?.rootViewController = UINavigationController(rootViewController: appDependencyContainer.generateHomeViewController()!)
43 | window?.makeKeyAndVisible()
44 | }
45 |
46 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
47 | if url.absoluteString.contains(AppURL.notificaiton.path) {
48 | showRegisterViewController()
49 | }
50 | return true
51 | }
52 |
53 | func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
54 | if shortcutItem.type == "SetAlarm" {
55 | showRegisterViewController()
56 | }
57 | }
58 |
59 | private func showRegisterViewController() {
60 | let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
61 | let initViewController = storyBoard.instantiateViewController(withIdentifier: "MainNavigationController")
62 | let registerAlertViewController = storyBoard.instantiateViewController(withIdentifier: RegisterAlertViewController.reusableIdentifier)
63 | if self.window?.rootViewController == nil {
64 | self.window?.rootViewController = initViewController
65 | self.window?.makeKeyAndVisible()
66 | } else {
67 | self.window?.rootViewController?.present(registerAlertViewController, animated: true, completion: nil)
68 | }
69 | }
70 |
71 | private func addShortcuts(to application: UIApplication) {
72 | let alarmShortcut = UIMutableApplicationShortcutItem(type: "SetAlarm", localizedTitle: "Set Alarm".localized, localizedSubtitle: nil, icon: UIApplicationShortcutIcon(type: .alarm), userInfo: nil)
73 |
74 | application.shortcutItems = [alarmShortcut]
75 | }
76 |
77 | func applicationWillResignActive(_ application: UIApplication) {
78 | }
79 |
80 | func applicationDidEnterBackground(_ application: UIApplication) {
81 | }
82 |
83 | func applicationWillEnterForeground(_ application: UIApplication) {
84 | }
85 |
86 | func applicationDidBecomeActive(_ application: UIApplication) {
87 | UIApplication.shared.applicationIconBadgeNumber = 0
88 | }
89 |
90 | func applicationWillTerminate(_ application: UIApplication) {
91 |
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/GitBingo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "57x57",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-57x57@1x.png",
49 | "scale" : "1x"
50 | },
51 | {
52 | "size" : "57x57",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-57x57@2x.png",
55 | "scale" : "2x"
56 | },
57 | {
58 | "size" : "60x60",
59 | "idiom" : "iphone",
60 | "filename" : "Icon-App-60x60@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "60x60",
65 | "idiom" : "iphone",
66 | "filename" : "Icon-App-60x60@3x.png",
67 | "scale" : "3x"
68 | },
69 | {
70 | "size" : "20x20",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-20x20@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "20x20",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-20x20@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "29x29",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-29x29@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "29x29",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-29x29@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "40x40",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-40x40@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "40x40",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-40x40@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "50x50",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-Small-50x50@1x.png",
109 | "scale" : "1x"
110 | },
111 | {
112 | "size" : "50x50",
113 | "idiom" : "ipad",
114 | "filename" : "Icon-Small-50x50@2x.png",
115 | "scale" : "2x"
116 | },
117 | {
118 | "size" : "72x72",
119 | "idiom" : "ipad",
120 | "filename" : "Icon-App-72x72@1x.png",
121 | "scale" : "1x"
122 | },
123 | {
124 | "size" : "72x72",
125 | "idiom" : "ipad",
126 | "filename" : "Icon-App-72x72@2x.png",
127 | "scale" : "2x"
128 | },
129 | {
130 | "size" : "76x76",
131 | "idiom" : "ipad",
132 | "filename" : "Icon-App-76x76@1x.png",
133 | "scale" : "1x"
134 | },
135 | {
136 | "size" : "76x76",
137 | "idiom" : "ipad",
138 | "filename" : "Icon-App-76x76@2x.png",
139 | "scale" : "2x"
140 | },
141 | {
142 | "size" : "83.5x83.5",
143 | "idiom" : "ipad",
144 | "filename" : "Icon-App-83.5x83.5@2x.png",
145 | "scale" : "2x"
146 | },
147 | {
148 | "size" : "1024x1024",
149 | "idiom" : "ios-marketing",
150 | "filename" : "ItunesArtwork@2x.png",
151 | "scale" : "1x"
152 | }
153 | ],
154 | "info" : {
155 | "version" : 1,
156 | "author" : "xcode"
157 | }
158 | }
--------------------------------------------------------------------------------
/GitBingo/Presenters/RegisterViewPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegisterViewPresenter.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 03/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import UserNotifications
11 |
12 | protocol RegisterNotificationProtocol: class {
13 | func showAlert(alertState: GitBingoAlertState)
14 | func updateDescriptionLabel(with text: String)
15 | func dismissVC()
16 | }
17 |
18 | class RegisterViewPresenter {
19 | // MARK: Properties
20 | private weak var vc: RegisterNotificationProtocol?
21 | private let center = UNUserNotificationCenter.current()
22 | private var time: String
23 | private var removeNotificationCompletion: ((UIAlertAction) -> Void)?
24 |
25 | private var hasScheduledNotification: Bool {
26 | guard GroupUserDefaults.shared.load(of: .notification) != nil else { return false }
27 | return true
28 | }
29 |
30 | private let dateFormatter: DateFormatter = {
31 | let dateFormatter = DateFormatter()
32 | dateFormatter.locale = .current
33 | dateFormatter.dateFormat = "HH:mm"
34 | return dateFormatter
35 | }()
36 |
37 | // MARK: Life Cycle
38 | init() {
39 | self.time = dateFormatter.string(from: Date())
40 | }
41 |
42 | // MARK: Methods
43 | func attachView(_ vc: RegisterNotificationProtocol?) {
44 | self.vc = vc
45 | }
46 |
47 | func detatchView() {
48 | self.vc = nil
49 | }
50 |
51 | func setupTime(with date: Date) {
52 | self.time = dateFormatter.string(from: date)
53 | }
54 |
55 | func showAlert() {
56 | center.getNotificationSettings { (settings) in
57 | if settings.authorizationStatus == .authorized {
58 | let register = GitBingoAlertState.register(self.hasScheduledNotification, self.time, { _ in
59 | self.vc?.dismissVC()
60 | self.generateNotification()
61 | })
62 | self.vc?.showAlert(alertState: register)
63 | } else {
64 | self.vc?.showAlert(alertState: .unauthorized)
65 | }
66 | }
67 | }
68 |
69 | func updateScheduledNotificationIndicator() {
70 | if let time = GroupUserDefaults.shared.load(of: .notification) as? String {
71 | vc?.updateDescriptionLabel(with: "Scheduled at %@".localized(with: time))
72 | return
73 | }
74 |
75 | vc?.updateDescriptionLabel(with: "No Scheduled Notification so far".localized)
76 | }
77 |
78 | private func generateNotification() {
79 | guard let times = pasreTime(from: time) else { return }
80 | let content = UNMutableNotificationContent()
81 | content.title = "Wait!".localized
82 | content.body = "Did You Commit?🤔".localized
83 | content.sound = UNNotificationSound.default
84 | content.badge = 1
85 |
86 | let userCalendar = Calendar.current
87 | var components = userCalendar.dateComponents([.hour, .minute], from: Date())
88 |
89 | components.hour = times.hour
90 | components.minute = times.minute
91 |
92 | let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
93 | let request = UNNotificationRequest(identifier: "GitBingo", content: content, trigger: trigger)
94 |
95 | center.add(request) { (error) in
96 | if error != nil {
97 | self.vc?.showAlert(alertState: .registerFailed)
98 | }
99 | GroupUserDefaults.shared.save(self.time, of: .notification)
100 | }
101 | }
102 |
103 | func removeNotification() {
104 | if hasScheduledNotification {
105 | let remove = GitBingoAlertState.removeNotification { (_) in
106 | self.center.removeAllPendingNotificationRequests()
107 | GroupUserDefaults.shared.remove(of: .notification)
108 | self.updateScheduledNotificationIndicator()
109 | }
110 |
111 | self.vc?.showAlert(alertState: remove)
112 | }
113 | }
114 |
115 | private func pasreTime(from time: String) -> (hour: Int, minute: Int)? {
116 | let times = self.time.split(separator: ":").map {String($0)}
117 | guard let hour = Int(times[0]) else { return nil }
118 | guard let minute = Int(times[1]) else { return nil }
119 | return (hour, minute)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Privacy Policy.md:
--------------------------------------------------------------------------------
1 | ## Privacy Policy
2 |
3 | Dongkun Lee built the GitBingo app as a Free app. This SERVICE is provided by Dongkun Lee at no cost and is intended for use as is.
4 |
5 | This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.
6 |
7 | If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy.
8 |
9 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at GitBingo unless otherwise defined in this Privacy Policy.
10 |
11 | **Information Collection and Use**
12 |
13 | For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information, including but not limited to Github ID. The information that I request will be retained on your device and is not collected by me in any way.
14 |
15 | The app does use third party services that may collect information used to identify you.
16 |
17 | Link to privacy policy of third party service providers used by the app
18 |
19 |
20 |
21 | **Log Data**
22 |
23 | I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.
24 |
25 | **Cookies**
26 |
27 | Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
28 |
29 | This Service does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service.
30 |
31 | **Service Providers**
32 |
33 | I may employ third-party companies and individuals due to the following reasons:
34 |
35 | - To facilitate our Service;
36 | - To provide the Service on our behalf;
37 | - To perform Service-related services; or
38 | - To assist us in analyzing how our Service is used.
39 |
40 | I want to inform users of this Service that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.
41 |
42 | **Security**
43 |
44 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.
45 |
46 | **Links to Other Sites**
47 |
48 | This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
49 |
50 | **Children’s Privacy**
51 |
52 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do necessary actions.
53 |
54 | **Changes to This Privacy Policy**
55 |
56 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
57 |
58 | **Contact Us**
59 |
60 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me.
61 |
62 | This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net/) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.firebaseapp.com/)
--------------------------------------------------------------------------------
/GitBingo/ViewControllers/IDInput/IDInputViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IDInputViewController.swift
3 | // GitBingo
4 | //
5 | // Created by 이동건 on 27/06/2019.
6 | // Copyright © 2019 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 | import SVProgressHUD
13 |
14 | class IDInputViewController: UIViewController {
15 | enum AlertViewState {
16 | case show
17 | case hide
18 |
19 | var anchorConstant: CGFloat {
20 | switch self {
21 | case .show:
22 | return 144
23 | case .hide:
24 | return -250
25 | }
26 | }
27 | }
28 | @IBOutlet weak var alertView: UIView!
29 | @IBOutlet weak var idTextField: UITextField!
30 | @IBOutlet weak var cancelButton: UIButton!
31 | @IBOutlet weak var doneButton: UIButton!
32 | @IBOutlet weak var errorMessageLabel: UILabel!
33 | @IBOutlet weak var alertViewTopAnchor: NSLayoutConstraint!
34 |
35 | private var idInputViewModel: IDInputViewModelType?
36 | private var disposeBag = DisposeBag()
37 |
38 | override func viewDidLoad() {
39 | super.viewDidLoad()
40 | setupViews()
41 | bind()
42 | }
43 |
44 | override func viewDidAppear(_ animated: Bool) {
45 | super.viewDidAppear(animated)
46 | animateAlertView(.show)
47 | }
48 |
49 | private func setupViews() {
50 | setupErrorMessageLabel()
51 | setupAlertView()
52 | setupCancelButton()
53 | setupDoneButton()
54 | }
55 |
56 | private func setupErrorMessageLabel() {
57 | errorMessageLabel.layer.cornerRadius = 7
58 | errorMessageLabel.layer.masksToBounds = true
59 | errorMessageLabel.isHidden = true
60 | }
61 |
62 | private func setupAlertView() {
63 | alertView.layer.cornerRadius = 20
64 | }
65 |
66 | private func setupCancelButton() {
67 | cancelButton.setTitleColor(ContributionGrade.hardGreen.color, for: .normal)
68 | cancelButton.addTarget(self, action: #selector(handleCancel), for: .touchUpInside)
69 | }
70 |
71 | private func setupDoneButton() {
72 | doneButton.setTitleColor(ContributionGrade.noneGreen.color, for: .disabled)
73 | doneButton.setTitleColor(ContributionGrade.extremeGreen.color, for: .normal)
74 | doneButton.addTarget(self, action: #selector(handleDone), for: .touchUpInside)
75 | }
76 |
77 | private func bind() {
78 | bindViewAttributes()
79 | }
80 |
81 | private func bindViewAttributes() {
82 | guard let idInputViewModel = idInputViewModel else { return }
83 |
84 | idTextField.rx.text.orEmpty
85 | .bind(to: idInputViewModel.inputText)
86 | .disposed(by: disposeBag)
87 |
88 | idInputViewModel.doneButtonValidation
89 | .bind(to: doneButton.rx.isEnabled)
90 | .disposed(by: disposeBag)
91 |
92 | idInputViewModel.responseStatus.observeOn(MainScheduler.instance)
93 | .subscribe(onNext: { status in
94 | switch status {
95 | case .success:
96 | self.handleDismiss()
97 | case .failed(let error):
98 | self.showError(error)
99 | self.animateAlertView(.show)
100 | }
101 | }).disposed(by: disposeBag)
102 |
103 | idInputViewModel.isLoading
104 | .asDriver(onErrorJustReturn: false)
105 | .drive(onNext: {
106 | $0 ? SVProgressHUD.show() : SVProgressHUD.dismiss()
107 | })
108 | .disposed(by: disposeBag)
109 | }
110 |
111 | private func showError(_ error: Error) {
112 | guard let error = error as? GitBingoError else { return }
113 | errorMessageLabel.text = error.description
114 | self.errorMessageLabel.isHidden = false
115 | }
116 |
117 | private func handleDismiss() {
118 | animateAlertView(.hide, completion: {
119 | self.dismiss(animated: false, completion: nil)
120 | })
121 | }
122 |
123 | @objc func handleCancel() {
124 | handleDismiss()
125 | }
126 |
127 | @objc func handleDone() {
128 | animateAlertView(.hide)
129 | errorMessageLabel.isHidden = true
130 | idInputViewModel?.fetch()
131 | }
132 |
133 | private func animateAlertView(_ state: AlertViewState, completion: (() -> Void)? = nil ) {
134 | alertViewTopAnchor.constant = state.anchorConstant
135 | state == .show ? idTextField.becomeFirstResponder() : idTextField.resignFirstResponder()
136 | UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
137 | self.view.layoutIfNeeded()
138 | }) { _ in
139 | completion?()
140 | }
141 | }
142 | }
143 |
144 | extension IDInputViewController: Storyboarded {
145 | static func instantiate(with viewModel: IDInputViewModelType) -> IDInputViewController? {
146 | let idInputViewController = IDInputViewController.instantiate()
147 | idInputViewController?.idInputViewModel = viewModel
148 | return idInputViewController
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/GitBingo/ViewControllers/Home/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Gitergy
4 | //
5 | // Created by 이동건 on 23/08/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 | import RxDataSources
13 | import SVProgressHUD
14 |
15 | class HomeViewController: UIViewController {
16 | // MARK: Typealias
17 | typealias CellConfiguration = (CollectionViewSectionedDataSource, UICollectionView, IndexPath, ContributionGrade) -> UICollectionViewCell
18 | typealias SupplementaryViewConfiguration = ((CollectionViewSectionedDataSource, UICollectionView, String, IndexPath) -> UICollectionReusableView)?
19 | typealias DotsSectionModel = SectionModel
20 | typealias DotsDataSources = RxCollectionViewSectionedReloadDataSource
21 |
22 | // MARK: Outlets
23 | @IBOutlet weak var githubInputAlertButton: UIButton!
24 | @IBOutlet weak var collectionView: UICollectionView!
25 |
26 | // MARK: Properties
27 | private var refreshControl = UIRefreshControl()
28 | private var homeViewDependencyContainer: HomeViewDependencyContainer?
29 | private lazy var homeViewModel: HomeViewModelType = homeViewDependencyContainer!.generateHomeViewModel()
30 | private var disposeBag = DisposeBag()
31 |
32 | private lazy var cellConfiguration: CellConfiguration = { (dataSource, collectionView, indexPath, grade) in
33 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: UICollectionViewCell.reusableIdentifier, for: indexPath)
34 | cell.backgroundColor = grade.color
35 | return cell
36 | }
37 |
38 | private lazy var supplementaryViewConfiguration: SupplementaryViewConfiguration = { (dataSource, collectionView, kind, indexPath) in
39 | guard let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeaderView.reusableIdentifier, for: indexPath) as? SectionHeaderView else {
40 | return UICollectionReusableView()
41 | }
42 | view.weekLabel.text = dataSource.sectionModels[indexPath.section].model
43 | return view
44 | }
45 |
46 | private var dotsDataSource: DotsDataSources {
47 | let dataSource = DotsDataSources(configureCell: cellConfiguration)
48 | dataSource.configureSupplementaryView = supplementaryViewConfiguration
49 | return dataSource
50 | }
51 |
52 | // MARK: Life Cycle
53 | override func viewDidLoad() {
54 | super.viewDidLoad()
55 | setupViews()
56 | bind()
57 | }
58 |
59 | // MARK: Setup Views
60 | private func setupViews() {
61 | setupNaviagtionBar()
62 | setupCollectionView()
63 | setupRefreshControl()
64 | }
65 |
66 | fileprivate func setupNaviagtionBar() {
67 | self.navigationController?.navigationBar.setValue(true, forKey: "hidesShadow")
68 | title = "GitBingo"
69 | navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(handleTapRefresh))
70 | navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "icons8-time"), style: .plain, target: self, action: #selector(handleScheduleTime))
71 | }
72 |
73 | fileprivate func setupCollectionView() {
74 | collectionView.rx.setDelegate(self).disposed(by: disposeBag)
75 | collectionView.showsVerticalScrollIndicator = false
76 | collectionView.allowsSelection = false
77 | }
78 |
79 | fileprivate func setupRefreshControl() {
80 | if #available(iOS 10.0, *) {
81 | collectionView.refreshControl = refreshControl
82 | } else {
83 | collectionView.addSubview(refreshControl)
84 | }
85 |
86 | refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
87 | }
88 |
89 | @objc func handleTapRefresh(_ sender: UIBarButtonItem) {
90 |
91 | }
92 |
93 | @objc func handleScheduleTime(_ sender: UIBarButtonItem) {
94 |
95 | }
96 |
97 | private func bind() {
98 | bindButtonTitle()
99 | bindCollectionView()
100 | }
101 |
102 | private func bindButtonTitle() {
103 | homeViewModel.title
104 | .drive(githubInputAlertButton.rx.title(for: .normal))
105 | .disposed(by: disposeBag)
106 | }
107 |
108 | private func bindCollectionView() {
109 | homeViewModel.sections
110 | .drive(collectionView.rx.items(dataSource: dotsDataSource))
111 | .disposed(by: disposeBag)
112 | }
113 |
114 | // MARK: Actions
115 | @objc func refresh() {
116 |
117 | }
118 |
119 | @IBAction func handleRefresh(_ sender: Any) {
120 |
121 | }
122 |
123 | @IBAction func handleShowGithubInputAlert(_ sender: Any) {
124 | guard let idInputViewController = IDInputViewController.instantiate(with: homeViewDependencyContainer!.generateIDInputViewModel()) else { return }
125 | idInputViewController.modalPresentationStyle = .overCurrentContext
126 | present(idInputViewController, animated: false, completion: nil)
127 | }
128 | }
129 |
130 | // MARK: - UICollectionViewDelegateFlowLayout
131 | extension HomeViewController: UICollectionViewDelegateFlowLayout {
132 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
133 | let widht = self.view.frame.width / 7
134 | return CGSize(width: widht, height: widht)
135 | }
136 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
137 | return CGSize(width: self.view.frame.width, height: 30)
138 | }
139 | }
140 |
141 | extension HomeViewController: Storyboarded {
142 | static func instantiate(with homeViewDependencyContainer: HomeViewDependencyContainer) -> HomeViewController? {
143 | let viewController = HomeViewController.instantiate()
144 | viewController?.homeViewDependencyContainer = homeViewDependencyContainer
145 | return viewController
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/GitBingo Widget/TodayViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TodayViewController.swift
3 | // GitBingo Widget
4 | //
5 | // Created by 이동건 on 26/09/2018.
6 | // Copyright © 2018 이동건. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import NotificationCenter
11 |
12 | class TodayViewController: UIViewController, NCWidgetProviding {
13 | // MARK: Outlets
14 | @IBOutlet weak var todayLabel: UILabel!
15 | @IBOutlet weak var weekLabel: UILabel!
16 | @IBOutlet weak var notificationLabel: UILabel!
17 |
18 | // To Be Hidden
19 | @IBOutlet weak var labelStackView: UIStackView!
20 | @IBOutlet weak var widgetCollectionView: UICollectionView!
21 |
22 | @IBOutlet weak var reloadButton: UIButton!
23 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
24 | @IBOutlet weak var githubRegisterButton: UIButton!
25 | @IBOutlet weak var todayCommitLabel: UILabel!
26 | @IBOutlet weak var weekTotalLabel: UILabel!
27 | @IBOutlet weak var notificationTimeLabel: UILabel! {
28 | didSet {
29 | notificationTimeLabel.isUserInteractionEnabled = true
30 | notificationTimeLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleRegisterNotificaiton)))
31 | }
32 | }
33 |
34 | // MARK: Presenter
35 | private var presenter: TodayViewPresenter = TodayViewPresenter(service: APIService(parser: Parser(),
36 | session: URLSession(configuration: .default)))
37 |
38 | // MARK: Life Cycle
39 | override func viewDidLoad() {
40 | super.viewDidLoad()
41 | widgetCollectionView.delegate = self
42 | widgetCollectionView.dataSource = self
43 | localization()
44 | setupPresenter()
45 | }
46 |
47 | func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
48 | completionHandler(NCUpdateResult.newData)
49 | }
50 |
51 | // MARK: Setup
52 | private func setupPresenter() {
53 | presenter.attachView(self)
54 | presenter.load()
55 | }
56 |
57 | private func localization() {
58 | todayLabel.text = todayLabel.text?.localized
59 | weekLabel.text = weekLabel.text?.localized
60 | notificationLabel.text = notificationLabel.text?.localized
61 | }
62 |
63 | // MARK: Fetch
64 | @IBAction func reload(_ sender: UIButton) {
65 | presenter.load()
66 | }
67 |
68 | private func load() {
69 | presenter.load()
70 | }
71 |
72 | // MARK: Action
73 | @objc func handleRegisterNotificaiton(_ gesture: UITapGestureRecognizer) {
74 | presenter.handleUserInteraction(type: .notificaiton)
75 | }
76 |
77 | @IBAction func handleRegisterID(_ sender: UIButton) {
78 | presenter.handleUserInteraction(type: .authentication)
79 | }
80 |
81 | }
82 |
83 | // MARK: - UICollectionViewDataSource
84 | extension TodayViewController: UICollectionViewDataSource {
85 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
86 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: UICollectionViewCell.reusableIdentifier, for: indexPath)
87 | cell.backgroundColor = presenter.colors(at: indexPath)
88 | return cell
89 | }
90 |
91 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
92 | return 7
93 | }
94 | }
95 |
96 | // MARK: - UICollectionViewDelegateFlowLayout
97 | extension TodayViewController: UICollectionViewDelegateFlowLayout {
98 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
99 | let width = collectionView.frame.width / 7
100 | return CGSize(width: width, height: width)
101 | }
102 |
103 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
104 | return 0
105 | }
106 |
107 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
108 | return 0
109 | }
110 | }
111 |
112 | // MARK: - GitBingoWidgetProtocol
113 | extension TodayViewController: GitBingoWidgetProtocol {
114 | func hide(error: GitBingoError?) {
115 | var hasError = false
116 | if let error = error {
117 | githubRegisterButton.isHidden = false
118 | switch error {
119 | case .idIsEmpty:
120 | githubRegisterButton.setTitle("Tap to register Github ID", for: .normal)
121 | githubRegisterButton.isUserInteractionEnabled = true
122 | default:
123 | githubRegisterButton.setTitle(error.description, for: .normal)
124 | githubRegisterButton.isUserInteractionEnabled = false
125 | }
126 | hasError = true
127 | }
128 |
129 | githubRegisterButton.isHidden = !hasError
130 | labelStackView.isHidden = hasError
131 | widgetCollectionView.isHidden = hasError
132 | reloadButton.isHidden = hasError
133 | }
134 |
135 | func startLoad() {
136 | activityIndicator.isHidden = false
137 | activityIndicator.startAnimating()
138 | }
139 |
140 | func endLoad() {
141 | activityIndicator.stopAnimating()
142 | widgetCollectionView.reloadData()
143 | }
144 |
145 | func hide(isAuthenticated: Bool) {
146 | githubRegisterButton.isHidden = isAuthenticated
147 | labelStackView.isHidden = !isAuthenticated
148 | widgetCollectionView.isHidden = !isAuthenticated
149 | reloadButton.isHidden = !isAuthenticated
150 | }
151 |
152 | func initUI(with contributions: Contributions?, at time: String) {
153 | guard let contributions = contributions else { return }
154 | todayCommitLabel.text = "\(contributions.today)"
155 | weekTotalLabel.text = "\(contributions.total)"
156 | notificationTimeLabel.text = time
157 | }
158 |
159 | func open(_ url: URL) {
160 | extensionContext?.open(url, completionHandler: nil)
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/GitBingo/RegisterAlertViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
36 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/GitBingo/ViewControllers/IDInput/IDInputViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
61 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/GitBingo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
47 |
53 |
59 |
65 |
71 |
77 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/GitBingo/HomeViewController.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
74 |
81 |
88 |
95 |
102 |
109 |
110 |
111 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
--------------------------------------------------------------------------------
/GitBingo Widget/Base.lproj/MainInterface.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
38 |
44 |
45 |
46 |
47 |
48 |
49 |
55 |
61 |
62 |
63 |
64 |
65 |
66 |
72 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
101 |
107 |
113 |
119 |
125 |
131 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
175 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
--------------------------------------------------------------------------------