├── 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 | 26 | 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 | 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 | --------------------------------------------------------------------------------