├── .gitignore ├── API ├── API.h ├── ContentType.swift ├── HTTPMethod.swift ├── Info.plist ├── QueryStringsBuilder.swift ├── Request.swift ├── Session.swift └── Task.swift ├── APITests ├── APITests.swift └── Info.plist ├── README.md ├── TimetableView ├── Info.plist ├── TimetableView.h ├── TimetableView.swift ├── TimetableViewConfiguration.swift ├── TimetableViewDataSource.swift ├── TimetableViewItem.swift └── TimetableViewLayout.swift ├── TimetableViewTests ├── Info.plist └── TimetableViewTests.swift ├── iOSDCRC.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── API.xcscheme │ │ ├── TimetableView.xcscheme │ │ └── iOSDCRC.xcscheme └── xcuserdata │ └── fromkk.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── iOSDCRC ├── API │ └── Twitter │ │ ├── Request │ │ ├── TwitterRequest+Search.swift │ │ └── TwitterRequest+Token.swift │ │ ├── Response │ │ ├── TwitterResponse+Status.swift │ │ ├── TwitterResponse+Token.swift │ │ └── TwitterResponse+User.swift │ │ └── Twitter.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon.png │ │ ├── icon_120-1.png │ │ ├── icon_120.png │ │ ├── icon_152.png │ │ ├── icon_167.png │ │ ├── icon_180.png │ │ ├── icon_20.png │ │ ├── icon_29.png │ │ ├── icon_40-1.png │ │ ├── icon_40-2.png │ │ ├── icon_40.png │ │ ├── icon_58-1.png │ │ ├── icon_58.png │ │ ├── icon_60.png │ │ ├── icon_76.png │ │ ├── icon_80-1.png │ │ ├── icon_80.png │ │ └── icon_87.png │ ├── Contents.json │ └── logo.imageset │ │ ├── Contents.json │ │ ├── logo.png │ │ ├── logo@2x.png │ │ └── logo@3x.png ├── Base.lproj │ └── LaunchScreen.storyboard ├── Constants │ └── Constants.swift ├── Extensions │ ├── Date+Extensions.swift │ ├── NSLayoutConstraint+Extensions.swift │ ├── Reusable.swift │ └── UIView+Extensions.swift ├── Info.plist ├── Resources │ ├── Localizations │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ └── ja.lproj │ │ │ └── Localizable.strings │ ├── about.json │ └── iosdcrc.json ├── Supports │ ├── Cancellable.swift │ ├── ImageLoader.swift │ ├── Injectable.swift │ └── Localizations.swift ├── ViewControllers │ ├── About │ │ ├── AboutViewController.swift │ │ ├── Entity │ │ │ ├── AboutEntity.swift │ │ │ └── AboutSections.swift │ │ ├── Interactor │ │ │ └── AboutInteractor.swift │ │ ├── Interfaces │ │ │ └── AboutInterfaces.swift │ │ ├── Presenter │ │ │ └── AboutPresenter.swift │ │ └── Views │ │ │ ├── AboutCell.swift │ │ │ └── AboutSectionHeaderView.swift │ ├── Root │ │ ├── Entity │ │ │ └── RootEntity.swift │ │ ├── Interactor │ │ │ └── RootInteractor.swift │ │ ├── Interface │ │ │ └── RootInterfaces.swift │ │ ├── Presenter │ │ │ └── RootPresenter.swift │ │ ├── RootViewController.swift │ │ ├── Router │ │ │ └── RootWireframe.swift │ │ └── View │ │ │ ├── RootFooterView.swift │ │ │ └── RootLogoView.swift │ ├── Timeline │ │ ├── Interactor │ │ │ └── TimelineInteractor.swift │ │ ├── Interface │ │ │ └── TimelineInterfaces.swift │ │ ├── Presenter │ │ │ └── TimelinePresenter.swift │ │ ├── TimelineViewController.swift │ │ └── View │ │ │ ├── TimelineCell.swift │ │ │ └── TweetDateConverter.swift │ └── Timetable │ │ ├── Entity │ │ └── TimetableEntity.swift │ │ ├── Interactor │ │ └── TimetableInteractor.swift │ │ ├── Interface │ │ └── TimetableInterfaces.swift │ │ ├── Presenter │ │ └── TimetablePresenter.swift │ │ ├── TimetableViewController.swift │ │ └── View │ │ ├── TimesScrollView.swift │ │ ├── TimetableCell.swift │ │ └── TracksScrollView.swift └── Views │ ├── Atomic │ └── UIColor+rc.swift │ └── LoadingView.swift ├── iOSDCRCTests ├── Info.plist └── iOSDCRCTests.swift └── iOSDCRCUITests ├── Info.plist └── iOSDCRCUITests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,swift,xcode 3 | 4 | ### OSX ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Swift ### 33 | # Xcode 34 | # 35 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 36 | 37 | ## Build generated 38 | build/ 39 | DerivedData/ 40 | 41 | ## Various settings 42 | *.pbxuser 43 | !default.pbxuser 44 | *.mode1v3 45 | !default.mode1v3 46 | *.mode2v3 47 | !default.mode2v3 48 | *.perspectivev3 49 | !default.perspectivev3 50 | xcuserdata/ 51 | 52 | ## Other 53 | *.moved-aside 54 | *.xccheckout 55 | *.xcscmblueprint 56 | 57 | ## Obj-C/Swift specific 58 | *.hmap 59 | *.ipa 60 | *.dSYM.zip 61 | *.dSYM 62 | 63 | ## Playgrounds 64 | timeline.xctimeline 65 | playground.xcworkspace 66 | 67 | # Swift Package Manager 68 | # 69 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 70 | # Packages/ 71 | # Package.pins 72 | # Package.resolved 73 | .build/ 74 | 75 | # CocoaPods 76 | # 77 | # We recommend against adding the Pods directory to your .gitignore. However 78 | # you should judge for yourself, the pros and cons are mentioned at: 79 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 80 | # 81 | # Pods/ 82 | # 83 | # Add this line if you want to avoid checking in source code from the Xcode workspace 84 | # *.xcworkspace 85 | 86 | # Carthage 87 | # 88 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 89 | # Carthage/Checkouts 90 | 91 | Carthage/Build 92 | 93 | # fastlane 94 | # 95 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 96 | # screenshots whenever they are needed. 97 | # For more information about the recommended setup visit: 98 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 99 | 100 | fastlane/report.xml 101 | fastlane/Preview.html 102 | fastlane/screenshots/**/*.png 103 | fastlane/test_output 104 | 105 | ### Xcode ### 106 | # Xcode 107 | # 108 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 109 | 110 | ## User settings 111 | 112 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 113 | 114 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 115 | 116 | ### Xcode Patch ### 117 | *.xcodeproj/* 118 | !*.xcodeproj/project.pbxproj 119 | !*.xcodeproj/xcshareddata/ 120 | !*.xcworkspace/contents.xcworkspacedata 121 | /*.gcno 122 | 123 | # AppCode 124 | .idea 125 | 126 | # End of https://www.gitignore.io/api/osx,swift,xcode 127 | -------------------------------------------------------------------------------- /API/API.h: -------------------------------------------------------------------------------- 1 | // 2 | // API.h 3 | // API 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for API. 12 | FOUNDATION_EXPORT double APIVersionNumber; 13 | 14 | //! Project version string for API. 15 | FOUNDATION_EXPORT const unsigned char APIVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /API/ContentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentType.swift 3 | // API 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/08. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ContentType { 12 | case formUrlEncoded 13 | case multipartFormData(String) 14 | 15 | func toString() -> String { 16 | switch self { 17 | case .formUrlEncoded: 18 | return "application/x-www-form-urlencoded" 19 | case .multipartFormData(let boundary): 20 | return "multipart/form-data; boundary=\(boundary)" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /API/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // API 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/08. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum HTTPMethod: String { 12 | case get = "GET" 13 | case post = "POST" 14 | case put = "PUT" 15 | case delete = "DELETE" 16 | } 17 | -------------------------------------------------------------------------------- /API/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /API/QueryStringsBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringsBuilder.swift 3 | // API 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/08. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class QueryStringsBuilder { 12 | 13 | public enum Options { 14 | case urlEncoding 15 | } 16 | 17 | let dictionary: [String: Any] 18 | public init(dictionary: [String: Any]) { 19 | self.dictionary = dictionary 20 | } 21 | 22 | private var options: [Options] = [] 23 | public func build(with options: [Options] = [.urlEncoding]) -> String { 24 | self.options = options 25 | return convert(dictionary: dictionary, with: "") 26 | } 27 | 28 | public func data() -> Data? { 29 | return build().data(using: .utf8) 30 | } 31 | 32 | func convert(array: [Any], with key: String) -> String { 33 | return array.enumerated().compactMap({ current in 34 | let key = String(format: "%@[%d]", key, current.offset) 35 | return toString(with: current.element, and: key) 36 | }).joined(separator: "&") 37 | } 38 | 39 | func convert(dictionary: [String: Any], with parentKey: String) -> String { 40 | return dictionary.keys.sorted().compactMap({ key in 41 | let outputKey: String = parentKey.isEmpty ? key : String(format: "%@[%@]", parentKey, key) 42 | guard let value = dictionary[key] else { return nil } 43 | return toString(with: value, and: outputKey) 44 | }).joined(separator: "&") 45 | } 46 | 47 | func toString(with value: Any, and key: String) -> String? { 48 | if let stringValue = value as? String { 49 | return String(format: "%@=%@", key, escapeIfNeeded(with: stringValue)) 50 | } else if let arrayValue = value as? [Any] { 51 | return convert(array: arrayValue, with: key) 52 | } else if let dictValue = value as? [String: Any] { 53 | return convert(dictionary: dictValue, with: key) 54 | } else if let custom = value as? CustomStringConvertible { 55 | return String(format: "%@=%@", key, escapeIfNeeded(with: custom.description)) 56 | } else { 57 | return String(format: "%@=", key) 58 | } 59 | } 60 | 61 | private func escapeIfNeeded(with string: String) -> String { 62 | guard options.contains(.urlEncoding) else { return string } 63 | 64 | var characterSet = CharacterSet.alphanumerics 65 | characterSet.insert(charactersIn: "-._~") 66 | return string.addingPercentEncoding(withAllowedCharacters: characterSet) ?? "" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /API/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request.swift 3 | // TypeNews 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/06. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol RequestRepresentable { 12 | var domain: String { get } 13 | 14 | var path: String { get } 15 | 16 | var method: HTTPMethod { get } 17 | 18 | var httpBody: Data? { get } 19 | 20 | var query: [String: String]? { get } 21 | 22 | var headers: [String: String]? { get } 23 | } 24 | 25 | public extension RequestRepresentable { 26 | var cachePolicy: URLRequest.CachePolicy { return .reloadIgnoringCacheData } 27 | 28 | var timeoutInterval: TimeInterval { return 60 } 29 | 30 | var contentType: ContentType { return .formUrlEncoded } 31 | 32 | var httpBody: Data? { return nil } 33 | 34 | var query: [String: String]? { return nil } 35 | 36 | var headers: [String: String]? { return nil } 37 | } 38 | 39 | extension RequestRepresentable { 40 | var _request: URLRequest { 41 | var url = URL(string: domain)!.appendingPathComponent(path) 42 | 43 | if let query = query { 44 | var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)! 45 | urlComponents.queryItems = query.map({ (item) -> URLQueryItem in 46 | return URLQueryItem(name: item.key, value: item.value) 47 | }) 48 | url = urlComponents.url! 49 | } 50 | 51 | var urlRequest = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval) 52 | urlRequest.httpMethod = method.rawValue 53 | urlRequest.httpBody = httpBody 54 | 55 | var headers: [String: String] = self.headers ?? [:] 56 | if case .post = method { 57 | headers["Content-Type"] = contentType.toString() 58 | headers["Content-Length"] = String(httpBody?.count ?? 0) 59 | } 60 | urlRequest.allHTTPHeaderFields = headers 61 | 62 | return urlRequest 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /API/Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Session.swift 3 | // API 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/08. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Session { 12 | private init() {} 13 | 14 | public static var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy? 15 | 16 | @discardableResult 17 | public static func data(with request: RequestRepresentable, callback: @escaping (Data?, Error?) -> ()) -> Task { 18 | let request = request._request 19 | let task: URLSessionTask = URLSession.shared.dataTask(with: request) { (data, _, error) in 20 | DispatchQueue.main.async { 21 | callback(data, error) 22 | } 23 | } 24 | task.resume() 25 | 26 | return Task(urlSessionTask: task) 27 | } 28 | 29 | @discardableResult 30 | public static func json(with request: RequestRepresentable, and type: T.Type, callback: @escaping (T?, Error?) -> ()) -> Task { 31 | return data(with: request, callback: { (data, error) in 32 | guard let data = data else { 33 | callback(nil, error) 34 | return 35 | } 36 | 37 | let jsonDecoder = JSONDecoder() 38 | if let dateDecodingStrategy = dateDecodingStrategy { 39 | jsonDecoder.dateDecodingStrategy = dateDecodingStrategy 40 | } 41 | 42 | do { 43 | let result = try jsonDecoder.decode(type, from: data) 44 | callback(result, nil) 45 | } catch { 46 | callback(nil, error) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /API/Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task.swift 3 | // API 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/08. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Cancellable { 12 | func cancel() 13 | } 14 | 15 | public class Task: Cancellable { 16 | var urlSessionTask: URLSessionTask 17 | init(urlSessionTask: URLSessionTask) { 18 | self.urlSessionTask = urlSessionTask 19 | } 20 | 21 | public func cancel() { 22 | urlSessionTask.cancel() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /APITests/APITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APITests.swift 3 | // APITests 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import API 11 | 12 | class APITests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /APITests/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOSDCRC 2018 2 | 3 | iOS Apps for iOSDC Reject Conference. 4 | 5 | ## Features 6 | 7 | - About 8 | - Timetable 9 | - Timeline 10 | 11 | ## Required 12 | 13 | You must get `consumer_key` and `consumer_secret` from [https://apps.twitter.com](https://apps.twitter.com) and update `Constants.swift` for load timeline. 14 | -------------------------------------------------------------------------------- /TimetableView/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /TimetableView/TimetableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableView.h 3 | // TimetableView 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TimetableView. 12 | FOUNDATION_EXPORT double TimetableViewVersionNumber; 13 | 14 | //! Project version string for TimetableView. 15 | FOUNDATION_EXPORT const unsigned char TimetableViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /TimetableView/TimetableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableView.swift 3 | // TimetableView 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/21. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// タイムテーブルを表現するクラス 12 | open class TimetableView: UICollectionView, UICollectionViewDataSource { 13 | 14 | public var configuration: TimetableViewConfiguration { 15 | didSet { 16 | (collectionViewLayout as? TimetableViewLayout)?.configuration = configuration 17 | reloadData() 18 | } 19 | } 20 | 21 | public init(configuration: TimetableViewConfiguration) { 22 | self.configuration = configuration 23 | 24 | super.init(frame: .zero, collectionViewLayout: TimetableViewLayout(configuration: configuration)) 25 | 26 | defer { 27 | self.dataSource = self 28 | } 29 | } 30 | 31 | required public init?(coder aDecoder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 36 | return configuration.dataSource?.numberOfSections(in: self) ?? 0 37 | } 38 | 39 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 40 | return configuration.dataSource?.timetableView(self, numberOfItemsIn: section) ?? 0 41 | } 42 | 43 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 44 | guard let cell = configuration.dataSource?.timetableView(self, cellForItemAt: indexPath) else { 45 | fatalError("cell reuse failed") 46 | } 47 | return cell 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /TimetableView/TimetableViewConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableViewConfiguration.swift 3 | // TimetableView 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/21. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class TimetableViewConfiguration { 12 | 13 | public let itemWidth: CGFloat 14 | public let heightOfHour: CGFloat 15 | public let itemEdgeInsets: UIEdgeInsets 16 | public let startDate: Date? 17 | public let endDate: Date? 18 | public weak var dataSource: TimetableViewDataSource? 19 | 20 | /// 初期化 21 | /// 22 | /// - Parameters: 23 | /// - itemWidth: 項目の横幅 24 | /// - heightOfHour: 1時間分の高さ 25 | /// - itemEdgeInsets: 項目のスペース 26 | /// - startDate: 開始時間 27 | /// - endDate: 終了時間 28 | /// - dataSource: 必要なデータを表現する 29 | public init(itemWidth: CGFloat, heightOfHour: CGFloat, itemEdgeInsets: UIEdgeInsets = .zero, startDate: Date?, endDate: Date?, dataSource: TimetableViewDataSource?) { 30 | self.itemWidth = itemWidth 31 | self.heightOfHour = heightOfHour 32 | self.itemEdgeInsets = itemEdgeInsets 33 | self.startDate = startDate 34 | self.endDate = endDate 35 | self.dataSource = dataSource 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TimetableView/TimetableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableViewDataSource.swift 3 | // TimetableView 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/21. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// タイムテーブルを表示するのに必要な情報 12 | public protocol TimetableViewDataSource: class { 13 | func numberOfSections(in timetableView: TimetableView) -> Int 14 | func timetableView(_ timetableView: TimetableView, numberOfItemsIn section: Int) -> Int 15 | func timetableView(_ timetableView: TimetableView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 16 | func timetableView(_ timetableView: TimetableView, itemAt indexPath: IndexPath) -> TimetableViewItem? 17 | } 18 | -------------------------------------------------------------------------------- /TimetableView/TimetableViewItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableViewItem.swift 3 | // TimetableView 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/21. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 項目を表現するのに必要な情報 12 | public protocol TimetableViewItem { 13 | 14 | /// 項目の開始時間 15 | var startAt: Date { get } 16 | 17 | /// 項目の終了時間 18 | var endAt: Date { get } 19 | } 20 | -------------------------------------------------------------------------------- /TimetableView/TimetableViewLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableViewLayout.swift 3 | // TimetableView 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/21. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TimetableViewLayout: UICollectionViewLayout { 12 | var configuration: TimetableViewConfiguration 13 | init(configuration: TimetableViewConfiguration) { 14 | self.configuration = configuration 15 | 16 | super.init() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | var attributesDictionary: [IndexPath: UICollectionViewLayoutAttributes] = [:] 24 | 25 | override func prepare() { 26 | attributesDictionary = [:] 27 | 28 | guard 29 | let dataSource = configuration.dataSource, 30 | let timetableView = collectionView as? TimetableView else { 31 | return 32 | } 33 | 34 | for section in 0.. [UICollectionViewLayoutAttributes]? { 48 | return attributesDictionary.compactMap({ (item) -> UICollectionViewLayoutAttributes? in 49 | guard item.value.frame.intersects(rect) else { 50 | return nil 51 | } 52 | return item.value 53 | }) 54 | } 55 | 56 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 57 | return false 58 | } 59 | 60 | override var collectionViewContentSize: CGSize { 61 | guard 62 | let dataSource = configuration.dataSource, 63 | let timetableView = collectionView as? TimetableView, 64 | let startDate = configuration.startDate, 65 | let endDate = configuration.endDate else { 66 | return .zero 67 | } 68 | 69 | let section = dataSource.numberOfSections(in: timetableView) 70 | return CGSize(width: configuration.itemWidth * CGFloat(section), height: height(for: endDate.timeIntervalSince1970 - startDate.timeIntervalSince1970)) 71 | } 72 | 73 | private func bounds(at indexPath: IndexPath, with item: TimetableViewItem) -> CGRect { 74 | guard let startDate = configuration.startDate else { 75 | return .zero 76 | } 77 | 78 | let x: CGFloat = CGFloat(indexPath.section) * configuration.itemWidth 79 | let width = configuration.itemWidth 80 | 81 | let start = item.startAt.timeIntervalSince1970 - startDate.timeIntervalSince1970 82 | let hour = item.endAt.timeIntervalSince1970 - item.startAt.timeIntervalSince1970 83 | 84 | let y: CGFloat = self.height(for: start) 85 | let height: CGFloat = self.height(for: hour) 86 | 87 | return CGRect(x: x, y: y, width: width, height: height) 88 | } 89 | 90 | private func height(for timeInterval: TimeInterval) -> CGFloat { 91 | let heightOfHour = configuration.heightOfHour 92 | let hour = timeInterval / 3600 93 | return heightOfHour * CGFloat(hour) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /TimetableViewTests/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 | -------------------------------------------------------------------------------- /TimetableViewTests/TimetableViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableViewTests.swift 3 | // TimetableViewTests 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TimetableView 11 | 12 | class TimetableViewTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /iOSDCRC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOSDCRC.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOSDCRC.xcodeproj/xcshareddata/xcschemes/API.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /iOSDCRC.xcodeproj/xcshareddata/xcschemes/TimetableView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /iOSDCRC.xcodeproj/xcshareddata/xcschemes/iOSDCRC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 53 | 59 | 60 | 61 | 63 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 81 | 82 | 83 | 84 | 94 | 96 | 102 | 103 | 104 | 105 | 106 | 107 | 113 | 115 | 121 | 122 | 123 | 124 | 126 | 127 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /iOSDCRC.xcodeproj/xcuserdata/fromkk.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | API.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | TimetableView.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 2 16 | 17 | Twitter.xcscheme 18 | 19 | orderHint 20 | 2 21 | 22 | iOSDCRC.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 0 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /iOSDCRC/API/Twitter/Request/TwitterRequest+Search.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterRequest+Search.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import API 11 | 12 | extension Twitter.Request { 13 | 14 | /// 検索APIを叩く 15 | struct Search: TwitterAuthorizedRequestable { 16 | init(accessToken: String, q: String, count: Int = 100, sinceID: Int64? = nil, maxID: Int64? = nil) { 17 | self.accessToken = accessToken 18 | self.q = q 19 | self.count = count 20 | self.sinceID = sinceID 21 | self.maxID = maxID 22 | } 23 | 24 | var accessToken: String 25 | 26 | var path: String = "1.1/search/tweets.json" 27 | 28 | var method: HTTPMethod = .get 29 | 30 | var q: String 31 | 32 | var count: Int 33 | 34 | var resultType: String = "recent" 35 | 36 | var sinceID: Int64? = nil 37 | 38 | var maxID: Int64? = nil 39 | 40 | var query: [String : String]? { 41 | var query: [String: String] = [ 42 | "q": q, 43 | "count": String(count), 44 | "result_type": resultType 45 | ] 46 | 47 | if let sinceID = sinceID { 48 | query["since_id"] = String(sinceID) 49 | } 50 | 51 | if let maxID = maxID { 52 | query["max_id"] = String(maxID) 53 | } 54 | 55 | return query 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /iOSDCRC/API/Twitter/Request/TwitterRequest+Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterRequest+Token.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import API 11 | 12 | extension Twitter.Request { 13 | 14 | /// アクセストークンを取得する 15 | struct Token: TwitterRequestable { 16 | var path: String = "oauth2/token" 17 | 18 | var method: HTTPMethod = .post 19 | 20 | let consumerKey: String 21 | let consumerSecret: String 22 | init(consumerKey: String, consumerSecret: String) { 23 | self.consumerKey = consumerKey 24 | self.consumerSecret = consumerSecret 25 | } 26 | 27 | var query: [String : String]? { return nil } 28 | 29 | var headers: [String : String]? { 30 | let data = String(format: "%@:%@", consumerKey, consumerSecret).data(using: .utf8)! 31 | let credential = data.base64EncodedString() 32 | 33 | return [ 34 | "Authorization": "Basic \(credential)" 35 | ] 36 | } 37 | 38 | var httpBody: Data? { 39 | return QueryStringsBuilder(dictionary: ["grant_type": "client_credentials"]).build().data(using: .utf8) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /iOSDCRC/API/Twitter/Response/TwitterResponse+Status.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterResponse+Status.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Twitter.Response { 12 | 13 | /// 投稿 14 | struct Statuses: Codable { 15 | var statuses: [Status] 16 | 17 | private enum CodingKeys: String, CodingKey { 18 | case statuses 19 | } 20 | } 21 | 22 | struct Status: Codable { 23 | var createdAt: Date 24 | var id: Int64 25 | var text: String 26 | var user: Twitter.Response.User 27 | 28 | private enum CodingKeys: String, CodingKey { 29 | case createdAt = "created_at" 30 | case id 31 | case text 32 | case user 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOSDCRC/API/Twitter/Response/TwitterResponse+Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterResponse+Token.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Twitter.Response { 12 | 13 | /// アクセストークンのレスポンス 14 | struct Token: Codable { 15 | let tokenType: String 16 | let accessToken: String 17 | 18 | private enum CodingKeys: String, CodingKey { 19 | case tokenType = "token_type" 20 | case accessToken = "access_token" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iOSDCRC/API/Twitter/Response/TwitterResponse+User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterResponse+User.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Twitter.Response { 12 | 13 | /// ユーザー情報 14 | struct User: Codable { 15 | var id: Int 16 | var name: String 17 | var screenName: String 18 | var profileImageUrlHttps: URL 19 | 20 | private enum CodingKeys: String, CodingKey { 21 | case id 22 | case name 23 | case screenName = "screen_name" 24 | case profileImageUrlHttps = "profile_image_url_https" 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /iOSDCRC/API/Twitter/Twitter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Twitter.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import API 11 | 12 | /// Twitter APIとの接続を賄う 13 | struct Twitter {} 14 | 15 | extension Twitter { 16 | struct Request {} 17 | struct Response {} 18 | } 19 | 20 | extension Twitter.Response { 21 | static var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy { 22 | let dateFormatter = DateFormatter() 23 | dateFormatter.locale = Locale(identifier: "en_US") 24 | dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" 25 | return JSONDecoder.DateDecodingStrategy.formatted(dateFormatter) 26 | } 27 | } 28 | 29 | protocol TwitterRequestable: RequestRepresentable {} 30 | 31 | extension TwitterRequestable { 32 | var domain: String { return "https://api.twitter.com" } 33 | var query: [String : String]? { return nil } 34 | var headers: [String : String]? { return nil } 35 | var httpBody: Data? { return nil } 36 | } 37 | 38 | protocol TwitterAuthorizedRequestable: TwitterRequestable { 39 | var accessToken: String { get } 40 | } 41 | 42 | extension TwitterAuthorizedRequestable { 43 | var bearer: String { return String(format: "Bearer %@", accessToken) } 44 | 45 | var headers: [String : String]? { 46 | return [ 47 | "Authorization": bearer 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /iOSDCRC/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | 18 | // Create RootViewController 19 | let rootViewController = RootViewController(style: .plain) 20 | 21 | let view = rootViewController 22 | let interactor = RootInteractor() 23 | let router = RootWireframe(viewController: rootViewController) 24 | 25 | let rootPresenter: RootPresenterProtocol = RootPresenter(dependencies: 26 | ( 27 | view: view, 28 | interactor: interactor, 29 | router: router 30 | ) 31 | ) 32 | rootViewController.inject(dependency: rootPresenter) 33 | 34 | let navigationController = UINavigationController(rootViewController: rootViewController) 35 | let window = UIWindow(frame: UIScreen.main.bounds) 36 | window.rootViewController = navigationController 37 | window.backgroundColor = .white 38 | window.makeKeyAndVisible() 39 | self.window = window 40 | 41 | return true 42 | } 43 | 44 | func applicationWillResignActive(_ application: UIApplication) { 45 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 46 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 47 | } 48 | 49 | func applicationDidEnterBackground(_ application: UIApplication) { 50 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 51 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 52 | } 53 | 54 | func applicationWillEnterForeground(_ application: UIApplication) { 55 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 56 | } 57 | 58 | func applicationDidBecomeActive(_ application: UIApplication) { 59 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 60 | } 61 | 62 | func applicationWillTerminate(_ application: UIApplication) { 63 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 64 | } 65 | 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon_40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon_60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon_58.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon_87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon_80.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon_120.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon_120-1.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon_180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "icon_20.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "icon_40-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "icon_29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "icon_58-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "icon_40-2.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "icon_80-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "icon_76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "icon_152.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "icon_167.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "icon.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_120-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_120-1.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_120.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_152.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_167.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_180.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_20.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_29.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_40-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_40-1.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_40-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_40-2.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_40.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_58-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_58-1.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_58.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_60.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_76.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_80-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_80-1.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_80.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/AppIcon.appiconset/icon_87.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "logo@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "logo@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/logo.imageset/logo.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/logo.imageset/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/logo.imageset/logo@2x.png -------------------------------------------------------------------------------- /iOSDCRC/Assets.xcassets/logo.imageset/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fromkk/iOSDCRC/e6e128ce63b725ff75991704010b3b2392bdfc72/iOSDCRC/Assets.xcassets/logo.imageset/logo@3x.png -------------------------------------------------------------------------------- /iOSDCRC/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 | -------------------------------------------------------------------------------- /iOSDCRC/Constants/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Constants {} 12 | 13 | extension Constants { 14 | struct Twitter { 15 | static let consumerKey = "YOUR CUSTOMER KEY" 16 | static let consumerSecret = "YOUR CUSTOMER SECRET KEY" 17 | } 18 | 19 | class Date { 20 | public static let minutes: Double = 60.0 21 | public static let hour = Constants.Date.minutes * 60.0 22 | public static let day = Constants.Date.hour * 24.0 23 | public static let week = Constants.Date.day * 7.0 24 | public static let month = Constants.Date.day * 30.0 25 | public static let year = Constants.Date.day * 365.0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOSDCRC/Extensions/Date+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Extensions.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Date { 12 | func toString(with format: String, andLocale locale: Locale = Locale.current, andTimzone timezone: TimeZone = .current) -> String { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.locale = locale 15 | dateFormatter.timeZone = timezone 16 | dateFormatter.dateFormat = format 17 | return dateFormatter.string(from: self) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /iOSDCRC/Extensions/NSLayoutConstraint+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutConstraint+Extensions.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/23. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSLayoutConstraint { 12 | func priority(_ priority: UILayoutPriority) -> NSLayoutConstraint { 13 | self.priority = priority 14 | return self 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iOSDCRC/Extensions/Reusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reusable.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol Reusable: class {} 12 | 13 | public extension Reusable { 14 | public static var reuseIdentifier: String { 15 | return String(describing: self) 16 | } 17 | } 18 | 19 | // MARK: - ReusableTableViewCell 20 | public protocol ReusableTableViewCell: Reusable {} 21 | 22 | public extension ReusableTableViewCell { 23 | 24 | public static func register(to tableView: UITableView) { 25 | tableView.register(self, forCellReuseIdentifier: reuseIdentifier) 26 | } 27 | 28 | public static func dequeue(for tableView: UITableView, at indexPath: IndexPath) -> Self { 29 | return tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! Self 30 | } 31 | 32 | } 33 | 34 | // MARK: ReusableTableHeaderFooterView 35 | public protocol ReusableTableViewHeaderFooterView: Reusable {} 36 | 37 | public extension ReusableTableViewHeaderFooterView { 38 | 39 | public static func register(to tableView: UITableView) { 40 | tableView.register(self, forHeaderFooterViewReuseIdentifier: reuseIdentifier) 41 | } 42 | 43 | public static func dequeue(for tableView: UITableView) -> Self { 44 | return tableView.dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) as! Self 45 | } 46 | 47 | } 48 | 49 | // MARK: - ReusableCollectionViewCell 50 | public protocol ReusableCollectionViewCell: Reusable {} 51 | 52 | public extension ReusableCollectionViewCell { 53 | 54 | public static func register(to collectionView: UICollectionView) { 55 | collectionView.register(self, forCellWithReuseIdentifier: reuseIdentifier) 56 | } 57 | 58 | public static func dequeue(for collectionView: UICollectionView, at indexPath: IndexPath) -> Self { 59 | return collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! Self 60 | } 61 | 62 | } 63 | 64 | -------------------------------------------------------------------------------- /iOSDCRC/Extensions/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | func addSubview(_ view: UIView, constraints: [NSLayoutConstraint]) { 13 | view.translatesAutoresizingMaskIntoConstraints = false 14 | addSubview(view) 15 | NSLayoutConstraint.activate(constraints) 16 | } 17 | } 18 | 19 | extension UIView { 20 | struct SafeArea { 21 | weak var view: UIView! 22 | 23 | var leadingAnchor: NSLayoutXAxisAnchor { 24 | if #available(iOS 11.0, *) { 25 | return view.safeAreaLayoutGuide.leadingAnchor 26 | } else { 27 | return view.leadingAnchor 28 | } 29 | } 30 | 31 | var trailingAnchor: NSLayoutXAxisAnchor { 32 | if #available(iOS 11.0, *) { 33 | return view.safeAreaLayoutGuide.trailingAnchor 34 | } else { 35 | return view.trailingAnchor 36 | } 37 | } 38 | 39 | var leftAnchor: NSLayoutXAxisAnchor { 40 | if #available(iOS 11.0, *) { 41 | return view.safeAreaLayoutGuide.leftAnchor 42 | } else { 43 | return view.leftAnchor 44 | } 45 | } 46 | 47 | var rightAnchor: NSLayoutXAxisAnchor { 48 | if #available(iOS 11.0, *) { 49 | return view.safeAreaLayoutGuide.rightAnchor 50 | } else { 51 | return view.rightAnchor 52 | } 53 | } 54 | 55 | var topAnchor: NSLayoutYAxisAnchor { 56 | if #available(iOS 11.0, *) { 57 | return view.safeAreaLayoutGuide.topAnchor 58 | } else { 59 | return view.topAnchor 60 | } 61 | } 62 | 63 | var bottomAnchor: NSLayoutYAxisAnchor { 64 | if #available(iOS 11.0, *) { 65 | return view.safeAreaLayoutGuide.bottomAnchor 66 | } else { 67 | return view.bottomAnchor 68 | } 69 | } 70 | 71 | var centerXAnchor: NSLayoutXAxisAnchor { 72 | if #available(iOS 11.0, *) { 73 | return view.safeAreaLayoutGuide.centerXAnchor 74 | } else { 75 | return view.centerXAnchor 76 | } 77 | } 78 | 79 | var centerYAnchor: NSLayoutYAxisAnchor { 80 | if #available(iOS 11.0, *) { 81 | return view.safeAreaLayoutGuide.centerYAnchor 82 | } else { 83 | return view.centerYAnchor 84 | } 85 | } 86 | 87 | var widthAnchor: NSLayoutDimension { 88 | if #available(iOS 11.0, *) { 89 | return view.safeAreaLayoutGuide.widthAnchor 90 | } else { 91 | return view.widthAnchor 92 | } 93 | } 94 | 95 | var heightAnchor: NSLayoutDimension { 96 | if #available(iOS 11.0, *) { 97 | return view.safeAreaLayoutGuide.heightAnchor 98 | } else { 99 | return view.heightAnchor 100 | } 101 | } 102 | 103 | var safeAreaInsets: UIEdgeInsets { 104 | if #available(iOS 11.0, *) { 105 | return view.safeAreaInsets 106 | } else { 107 | return .zero 108 | } 109 | } 110 | } 111 | 112 | var safeArea: SafeArea { return SafeArea(view: self) } 113 | } 114 | -------------------------------------------------------------------------------- /iOSDCRC/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /iOSDCRC/Resources/Localizations/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | iOSDCRC 4 | 5 | Created by Kazuya Ueoka on 2018/07/23. 6 | Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | */ 8 | 9 | "General.OK" = "OK"; 10 | 11 | "Tweet.Date.Seconds" = "s"; 12 | 13 | "Tweet.Date.Minutes" = "m"; 14 | 15 | "Tweet.Date.Hour" = "h"; 16 | 17 | "Tweet.Date.Day" = "d"; 18 | 19 | "Timeline.Alert.TokenGetFailed.Title" = "Error"; 20 | 21 | "Timeline.Alert.TokenGetFailed.Message" = "Token get failed."; 22 | 23 | "Timeline.Alert.TweetGetFailed.Title" = "Error"; 24 | 25 | "Timeline.Alert.TweetGetFailed.Message" = "Tweet get failed."; 26 | -------------------------------------------------------------------------------- /iOSDCRC/Resources/Localizations/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | iOSDCRC 4 | 5 | Created by Kazuya Ueoka on 2018/07/23. 6 | Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | */ 8 | 9 | "General.OK" = "OK"; 10 | 11 | "Tweet.Date.Seconds" = "秒"; 12 | 13 | "Tweet.Date.Minutes" = "分"; 14 | 15 | "Tweet.Date.Hour" = "時間"; 16 | 17 | "Tweet.Date.Day" = "日"; 18 | 19 | "Timeline.Alert.TokenGetFailed.Title" = "エラー"; 20 | 21 | "Timeline.Alert.TokenGetFailed.Message" = "トークンの取得に失敗しました。"; 22 | 23 | "Timeline.Alert.TweetGetFailed.Title" = "エラー"; 24 | 25 | "Timeline.Alert.TweetGetFailed.Message" = "ツイートの取得に失敗しました。"; 26 | -------------------------------------------------------------------------------- /iOSDCRC/Resources/about.json: -------------------------------------------------------------------------------- 1 | { 2 | "dates": [ 3 | { 4 | "from": "2018-09-18T19:00:00+09:00", 5 | "to": "2018-09-18T22:00:00+09:00" 6 | }, 7 | { 8 | "from": "2018-09-20T19:00:00+09:00", 9 | "to": "2018-09-20T22:00:00+09:00" 10 | } 11 | ], 12 | "locations": [ 13 | { 14 | "name": "DeNA渋谷 ヒカリエ オフィス", 15 | "postal_code": "150-8510", 16 | "address": "東京都渋谷区渋谷2-21-1" 17 | }, 18 | { 19 | "name": "LINE Fukuoka", 20 | "postal_code": "812-0012", 21 | "address": "福岡県福岡市博多区博多駅中央街8-1 JRJP博多ビル12F" 22 | } 23 | ], 24 | "sponsors": [ 25 | { 26 | "name": "株式会社ディー・エヌ・エー", 27 | "description": "会場提供・Day 1懇親会費用提供・配信設備提供" 28 | }, 29 | { 30 | "name": "株式会社リクルートテクノロジーズ", 31 | "description": "Day 2懇親会費用提供" 32 | } 33 | ], 34 | "speakers": [ 35 | { 36 | "account": "@giginet", 37 | "name": "ぎぎにゃん" 38 | }, 39 | { 40 | "account": "@fumiyasac", 41 | "name": "fumiyasac" 42 | }, 43 | { 44 | "account": "@lovee", 45 | "name": "lovee" 46 | }, 47 | { 48 | "account": "@takattata", 49 | "name": "Takasy" 50 | }, 51 | { 52 | "account": "@yshogo87", 53 | "name": "shogo.yamada" 54 | }, 55 | { 56 | "account": "@nonchalant0303", 57 | "name": "Nonchalant" 58 | }, 59 | { 60 | "account": "@_mogaming", 61 | "name": "mogaming" 62 | }, 63 | { 64 | "account": "@naruchigi", 65 | "name": "Naruki Chigira" 66 | }, 67 | { 68 | "account": "@vespid", 69 | "name": "みつよし" 70 | }, 71 | { 72 | "account": "@swiz_ard", 73 | "name": "Kubode" 74 | }, 75 | { 76 | "account": "@kazuhiro494949", 77 | "name": "kazuhiro4949" 78 | }, 79 | { 80 | "account": "@kz56cd", 81 | "name": "kz56cd" 82 | }, 83 | { 84 | "account": "@fumito_ito", 85 | "name": "Fumito Ito" 86 | }, 87 | { 88 | "account": "@takehilo_kaneko", 89 | "name": "takehilo" 90 | } 91 | ], 92 | "staffs": [ 93 | { 94 | "account": "@akatsuki174", 95 | "name": "akatsuki174" 96 | }, 97 | { 98 | "account": "@fromkk", 99 | "name": "かっくん" 100 | }, 101 | { 102 | "account": "@nesskazu", 103 | "name": "NESS" 104 | }, 105 | { 106 | "account": "@ukitaka_", 107 | "name": "ukitaka" 108 | }, 109 | { 110 | "account": "@gaopin1534", 111 | "name": "gaopin" 112 | }, 113 | { 114 | "account": "@_ha1f", 115 | "name": "はるふの葉の庭" 116 | }, 117 | { 118 | "account": "@itaru_sugimoto", 119 | "name": "いたる" 120 | }, 121 | { 122 | "account": "@kiko_tw", 123 | "name": "kiko" 124 | }, 125 | { 126 | "account": "@macneko_ayu", 127 | "name": "こうちゃん黒猫まみれ" 128 | }, 129 | { 130 | "account": "@mizu_gako", 131 | "name": "トントン ランラン☆ガーコ" 132 | }, 133 | { 134 | "account": "@niwatako", 135 | "name": "にわタコ" 136 | }, 137 | { 138 | "account": "@takattata", 139 | "name": "Takasy" 140 | } 141 | ] 142 | } 143 | -------------------------------------------------------------------------------- /iOSDCRC/Resources/iosdcrc.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "iOSDC Reject Conference day1", 4 | "description": "Day 1", 5 | "start_at": "2018-09-18T19:20:00+09:00", 6 | "end_at": "2018-09-18T21:00:00+09:00", 7 | "tracks": [ 8 | { 9 | "name": "Track A", 10 | "sessions": [ 11 | { 12 | "title": "Swiftをより良くする", 13 | "description": "It has been three years since Swift was first introduced. Yet there are still missing features and unfixed bugs. The good news is that Swift is open source and we, the community have the power to fix it. In this talk, I’ll discuss how to pitch an idea in Swift Forums, submit a Proposal, and how to build, test and benchmark Swift using the example of Dictionary.compactMapValues that I proposed and got merged for Swift 5.", 14 | "url": "https://www.tryswift.co/events/2018/nyc/#betterswift", 15 | "author": "@d_date", 16 | "start_at": "2018-09-18T19:25:00+09:00", 17 | "end_at": "2018-09-18T19:55:00+09:00" 18 | }, 19 | { 20 | "title": "エンジニア経験を活かしたスクラムマスターとして開発チームとプロダクトを成長させる", 21 | "description": "スクラムに関する情報と書籍は世の中に溢れています。もし、間違った解釈によってエンジニアがより少ない情報・短い時間での開発を強いられるとしたら、改善をする必要があります。このトークでは、スクラムの原論文と言われる"The New New Product Development Game"を読み解き、エンジニアの成長を促すような環境と組織に利益をもたらす開発の両立を目指すための活動について話します。", 22 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/3cc0e52b-1aa2-431b-a215-960bbe64f4b3", 23 | "author": "@_naru_jpn", 24 | "start_at": "2018-09-18T20:05:00+09:00", 25 | "end_at": "2018-09-18T20:20:00+09:00" 26 | }, 27 | { 28 | "title": "非同期UI描画による高速なアプリケーションの実装", 29 | "description": "iOSアプリケーションの高速化においてメインスレッドをブロックするUIの描画コストは無視できない問題です。\n\n日経電子版アプリでは非同期なUI描画を実現するTexture(AsyncDisplayKit)を導入し、大量のテーブルをスムーズに描画しています。\n本セッションではTextureの検討から具体的な導入ステップ、導入にあたって遭遇したトラブルと対応方法について紹介します。", 30 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/cf5808bf-b232-40fc-a3f0-5a1c3c1897b4", 31 | "author": "@fumito_ito", 32 | "start_at": "2018-09-18T20:20:00+09:00", 33 | "end_at": "2018-09-18T20:35:00+09:00" 34 | }, 35 | { 36 | "title": "特別企画\n今だから応募できるトーク\nBitcoinの署名の仕組み、Bitcoinのscriptを書いてみよう", 37 | "description": "今だから応募できるトーク\nBitcoinの署名の仕組み、Bitcoinのscriptを書いてみよう", 38 | "url": null, 39 | "author": "@usatie", 40 | "start_at": "2018-09-18T20:40:00+09:00", 41 | "end_at": "2018-09-18T20:55:00+09:00" 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "Track B", 47 | "sessions": [ 48 | { 49 | "title": "Factoryの自動生成によりテストを書きやすくする", 50 | "description": "テストを書くのって面倒ですよね?理由は様々ですが、ひとつは入力値と期待値を用意する手間だと思います。ベタに用意すると再利用性が低く、また構造変化に弱いというデメリットがあります。その解決策としてFactoryがあります。\n\nただ、実際はFactoryを用意するのも手間でコストがかかってしまいます。今回はFactoryを自動生成することで、再利用性が高く構造変化に強いツールを作りました。そのツールの特徴とコード生成についてお話します。", 51 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/4164f4e2-91a0-468a-b420-80b1f6a6576a", 52 | "author": "@nonchalant0303", 53 | "start_at": "2018-09-18T19:25:00+09:00", 54 | "end_at": "2018-09-18T19:55:00+09:00" 55 | }, 56 | { 57 | "title": "退屈なことはApp Store Connect APIにやらせよう", 58 | "description": "ついに発表されたApp Store Connect API。これを使えば何ができるのでしょうか?\n公開されている情報からいくつかの業務改善のアイディアをお話しします。\nまた、実際にApp Store Connect APIクライアントを実装してみて、Swiftで簡単なタスクを記述する方法をお伝えします。\n\nこのトークの内容はトーク時にApp Store Connect APIが公開されているかによって変わります。", 59 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/e48abccc-e824-4d36-bb4d-b373419cfaad", 60 | "author": "@giginet", 61 | "start_at": "2018-09-18T20:05:00+09:00", 62 | "end_at": "2018-09-18T20:20:00+09:00" 63 | }, 64 | { 65 | "title": "RxSwfitを採用したプロジェクトにおけるテストパターン", 66 | "description": "RxSwiftを採用したプロジェクトでは、多くの関数をObservableを返す関数として実装します。Observableを返す関数は通常の関数と違い、ユニットテストに少し工夫が必要になります。\nこのトークでは、日々Observableな関数に対してユニットテストを書いている中で見えてきたテストのパターンについてご紹介します。\n\n※関連ライブラリ\nQuick、Nimble、RxTest、RxBlocking、Mockingjay", 67 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/cf9c964e-c1a5-4c70-8a89-02f1a10b0bfe", 68 | "author": "@takehilo_kaneko", 69 | "start_at": "2018-09-18T20:20:00+09:00", 70 | "end_at": "2018-09-18T20:35:00+09:00" 71 | }, 72 | { 73 | "title": "Twitterのプロフィール画面の再実装を通して、ScrollViewを組み合わせたContainerVCの作り方を学ぶ", 74 | "description": "ScrollViewを組み合わせて快適に動くUIを作るには超えなければならないハードルがたくさんあります\n任意のViewControllerをはめ込めるContainerVCを作るのはさらに難しいですが、実現できればどのアプリのどの画面にも応用できるようになります\n\nこのトークでは、Twitterアプリのプロフィール画面を例にとって、ScrollViewを組み合わせたContainerVCはどのように実装すればよいのか解説します", 75 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/bf608f85-9928-4450-83b8-2db8956ccd97", 76 | "author": "@vespid", 77 | "start_at": "2018-09-18T20:35:00+09:00", 78 | "end_at": "2018-09-18T20:50:00+09:00" 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "Track C", 84 | "sessions": [ 85 | { 86 | "title": "特別企画\nココだから話せる!SNS NGトーク\n\n@fumiyasac Swiftバージョンアップを放ったらかしにすると...?\n@zakiyamaaaaa 深センでiPhoneの容量を4倍にしたけど、質問ある?", 87 | "description": "ココだから話せる!SNS NGトーク\n\n@fumiyasac Swiftバージョンアップを放ったらかしにすると...?\n@zakiyamaaaaa 深センでiPhoneの容量を4倍にしたけど、質問ある?", 88 | "url": null, 89 | "author": null, 90 | "start_at": "2018-09-18T19:25:00+09:00", 91 | "end_at": "2018-09-18T19:55:00+09:00" 92 | }, 93 | { 94 | "title": "特別企画\nテストライブコーディング\n\n運営が用意したOSSアプリに対して発表者がテストをライブコーディングしながらみんなでわいわいする企画", 95 | "description": "テストライブコーディング\n\n運営が用意したOSSアプリに対して発表者がテストをライブコーディングしながらみんなでわいわいする企画", 96 | "url": null, 97 | "author": "@ktanaka117", 98 | "start_at": "2018-09-18T19:55:00+09:00", 99 | "end_at": "2018-09-18T20:25:00+09:00" 100 | }, 101 | { 102 | "title": "特別企画\nスポンサーパネルセッション\nDeNA社・リクルートテクノロジーズ社\n\nどんな話題が出るかは欄外参照。リアルな現場の話が聞けるかも…??", 103 | "description": "スポンサーパネルセッション\nDeNA社・リクルートテクノロジーズ社\n\nどんな話題が出るかは欄外参照。リアルな現場の話が聞けるかも…??", 104 | "url": null, 105 | "author": null, 106 | "start_at": "2018-09-18T20:25:00+09:00", 107 | "end_at": "2018-09-18T20:55:00+09:00" 108 | } 109 | ] 110 | } 111 | ] 112 | }, 113 | { 114 | "title": "iOSDC Reject Conference day2", 115 | "description": "Day 2", 116 | "start_at": "2018-09-20T19:20:00+09:00", 117 | "end_at": "2018-09-20T21:00:00+09:00", 118 | "tracks": [ 119 | { 120 | "name": "Track A", 121 | "sessions": [ 122 | { 123 | "title": "デザイナーと一緒にコラボして仕上げるアニメーション実装とショーケース紹介", 124 | "description": "社内で新規アプリを開発するぞ、そして今回はシンプルなUIの中にも気持ちの良いアニメーションを盛り込むぞ!という所から始まったプロジェクトにてUI開発を担当した際に、得た知見やデザイナーとの協業の際に留意した点、シンプルな中にも心地よいアクセントをつけるためのポイントになる実装やデザイナーとの協業の際に使用したツール、UI表現をする過程で検討したライブラリ等やその他の点についてもご紹介できればと思います。", 125 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/5a1d14e4-a4f1-4d38-9eb5-29064aab8ac2", 126 | "author": "@fumiyasac", 127 | "start_at": "2018-09-20T19:25:00+09:00", 128 | "end_at": "2018-09-20T19:55:00+09:00" 129 | }, 130 | { 131 | "title": "UITesting with GraphQL", 132 | "description": "GraphQLを利用したiOSアプリケーションでUITestを実現する方法について説明します。\n\n# Agenda\n- GraphQLライブラリの「Apollo」を利用したiOSアプリケーション開発について\n- ServerSideSwiftを利用してCI上でWebMockを立ち上げUITestを行う方法について\n- WebMockでGraphQLのテストデータを効率よく記述するために開発したライブラリの紹介", 133 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/68ea9a5a-36ce-4816-b345-7de008f4ab25", 134 | "author": "@iida_0213", 135 | "start_at": "2018-09-20T20:05:00+09:00", 136 | "end_at": "2018-09-20T20:35:00+09:00" 137 | }, 138 | { 139 | "title": "「開発初心者は何がわからないのか」わからないを言語化するヒント", 140 | "description": "対象:メンターやメンティー\n概要:\ngitのブランチやマージの概念がわからなくてターミナルの黒い画面が怖い、コンフリクトを出そうもんなら発狂しそうになる。くらす?なにそれ美味しいの?状態でアプリ開発者になりました。\nわからないことを何がわからないのか、始めたばっかりの頃は言語化できません。\nアプリ初心者がどこに躓き、何がわからないのかを紹介します。", 141 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/28548bb8-4e07-41aa-ba9f-49041bac3027", 142 | "author": "@marina10172013", 143 | "start_at": "2018-09-20T20:40:00+09:00", 144 | "end_at": "2018-09-20T20:45:00+09:00" 145 | }, 146 | { 147 | "title": "iOSアプリのUniversal対応をやめようとした話", 148 | "description": "初めてiOSアプリをリリースする際、Deployment InfoのDevicesの選択肢として、「Universal, iPhone, iPad」と3つの選択肢があります。この中で、Universalを選択してリリースしたものの、様々な経緯によりUniversal対応をやめたいときがあります。やめようとしたけど結果的に無理だった話とともに、初回リリース時の選択は気をつけるべきという話をします。", 149 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/21295db1-7b7d-46be-b5e8-da3102f80f03", 150 | "author": "@_mogaming", 151 | "start_at": "2018-09-20T20:45:00+09:00", 152 | "end_at": "2018-09-20T20:50:00+09:00" 153 | }, 154 | { 155 | "title": "Flutter触りまくったからその知見を公開する", 156 | "description": "Flutterをたくさん触ったのでその知見を下記の観点からお話したいと思います。\n・個人開発で使えるか\n・ビジネスで使えるか\n・これからやり始めるのはまだ早いのか、今から始めるのがベストか", 157 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/366c3308-19c8-4c54-96e3-6358190ac26f", 158 | "author": "@yshogo87", 159 | "start_at": "2018-09-20T20:50:00+09:00", 160 | "end_at": "2018-09-20T20:55:00+09:00" 161 | } 162 | ] 163 | }, 164 | { 165 | "name": "Track B", 166 | "sessions": [ 167 | { 168 | "title": "Metalによる素材を活かしたUI作り", 169 | "description": "iOSにはGPUの性質を活かしたデザインが様々なところで使われています。シェーダーを使うとグラフィカルでインタラクティブなデザインが可能になります。本トークではGPUの特徴を活かしたグラフィックプログラミングで独自のUIを作る方法について説明します。", 170 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/b823ee9c-111a-41b3-9449-3eca2a850bea", 171 | "author": "@kazuhiro494949", 172 | "start_at": "2018-09-20T19:25:00+09:00", 173 | "end_at": "2018-09-20T19:55:00+09:00" 174 | }, 175 | { 176 | "title": "Manual DI with ReactorKit", 177 | "description": "昨今のiOS界隈でも、関連する発表・トークが多い印象のDIについて。\n\nReactorKitは、RxSwiftをベースにFlux Architectureを実現するためのフレームワークですが、\nRxSwiftの恩恵により、イメージよりも手軽に、小回りの効いたDIを組み込むことができます。\n\n今回は、そんなReactorKitにフィットするDIを自作してみましょう。", 178 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/e5b8d536-93c8-45cc-983b-19af9be40619", 179 | "author": "@kz56cd", 180 | "start_at": "2018-09-20T20:05:00+09:00", 181 | "end_at": "2018-09-20T20:20:00+09:00" 182 | }, 183 | { 184 | "title": "Self-Documenting Code のススメ方", 185 | "description": "我々エンジニアは嫌いなことが二つある。一つはドキュメントを書くこと、一つは人の書いたドキュメントのないコードを読むこと。\n\nそれなら、コード自体がドキュメントになっていれば、みんなハッピーなのでは?\n\nこのトークは、そんな優しい世界を実現してくれる、Self-Documenting Code について語ります。", 186 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/3a92b6a9-1e77-4875-a46e-83912a6b25e3", 187 | "author": "@lovee", 188 | "start_at": "2018-09-20T20:20:00+09:00", 189 | "end_at": "2018-09-20T20:35:00+09:00" 190 | }, 191 | { 192 | "title": "ジェネリクスを使ったプロフィール画面の構築", 193 | "description": "Wantedly Visitのプロフィール画面には多くの項目があり、それぞれに編集画面があります。\nWantedly Visitではプロフィールの項目1つ1つに編集画面を実装するのではなく、ジェネリクスを使って汎用的に構築することで実装コストを下げることができました。\nこのトークでは、Wantedly Visitのプロフィール画面におけるジェネリクスの利用例をご紹介します。", 194 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/21947ef0-2c75-4f12-bcb7-8cfd4c03fe2c", 195 | "author": "@swiz_ard", 196 | "start_at": "2018-09-20T20:40:00+09:00", 197 | "end_at": "2018-09-20T20:55:00+09:00" 198 | } 199 | ] 200 | }, 201 | { 202 | "name": "Track C", 203 | "sessions": [ 204 | { 205 | "title": "特別企画\nテストライブコーディング\n\n運営が用意したOSSアプリに対して発表者がテストをライブコーディングしながらみんなでわいわいする企画", 206 | "description": "テストライブコーディング\n\n運営が用意したOSSアプリに対して発表者がテストをライブコーディングしながらみんなでわいわいする企画", 207 | "url": null, 208 | "author": "@orga_chem", 209 | "start_at": "2018-09-20T19:25:00+09:00", 210 | "end_at": "2018-09-20T19:55:00+09:00" 211 | }, 212 | { 213 | "title": "特別企画\n今だから応募できるトーク\n\n@yshogo87 FlutterアプリをApp Store申請しても通らない、、、\n@bannzai UICollectionViewのデータ構造とViewの構造を統一する", 214 | "description": "今だから応募できるトーク\n\n@yshogo87 FlutterアプリをApp Store申請しても通らない、、、\n@bannzai UICollectionViewのデータ構造とViewの構造を統一する", 215 | "url": null, 216 | "author": null, 217 | "start_at": "2018-09-20T19:55:00+09:00", 218 | "end_at": "2018-09-20T20:25:00+09:00" 219 | }, 220 | { 221 | "title": "DDD(ドメイン駆動設計)を知っていますか??", 222 | "description": "DDDを学んだことはありますか?\nレイヤー化アーキテクチャとかの話でしょ?と思っていませんか?\nDDDは小手先で行うものではなく、仕組みを作っていくことが主です。\nDDDを学び実践することで、チーム各員の意識が変わり、今のプロダクトが整理されていくはずです。\nそんなDDDへの入り口を開くセッションに出来ればと思います。", 223 | "url": "https://fortee.jp/iosdc-japan-2018/proposal/0d1a261a-a268-4d82-af16-a2a9b357b2bc", 224 | "author": "@takattata", 225 | "start_at": "2018-09-20T20:25:00+09:00", 226 | "end_at": "2018-09-20T20:40:00+09:00" 227 | }, 228 | { 229 | "title": "特別企画\nココだから話せる!SNS NGトーク\n\n@zakiyamaaaaa 深センでiPhoneの容量を4倍にしたけど、質問ある?", 230 | "description": "ココだから話せる!SNS NGトーク\n\n@zakiyamaaaaa 深センでiPhoneの容量を4倍にしたけど、質問ある?", 231 | "url": null, 232 | "author": null, 233 | "start_at": "2018-09-20T20:40:00+09:00", 234 | "end_at": "2018-09-20T20:55:00+09:00" 235 | } 236 | ] 237 | } 238 | ] 239 | } 240 | ] 241 | -------------------------------------------------------------------------------- /iOSDCRC/Supports/Cancellable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cancellable.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/24. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Cancellable { 12 | func cancel() 13 | } 14 | -------------------------------------------------------------------------------- /iOSDCRC/Supports/ImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoader.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/24. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// URLから画像を読み込むためのクラス 12 | class ImageLoader { 13 | enum Errors: Error { 14 | case convertFailed 15 | } 16 | 17 | class Task: Cancellable { 18 | var urlSessionDataTask: URLSessionDataTask? 19 | var url: URL 20 | init(url: URL, urlSessionDataTask: URLSessionDataTask?) { 21 | self.url = url 22 | self.urlSessionDataTask = urlSessionDataTask 23 | } 24 | 25 | func cancel() { 26 | urlSessionDataTask?.cancel() 27 | } 28 | } 29 | 30 | static func load(with url: URL, completion: @escaping (UIImage?, Error?) -> ()) -> Task? { 31 | let urlRequest = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 30) 32 | let task = URLSession.shared.dataTask(with: urlRequest) { (data, _, error) in 33 | DispatchQueue.main.async { 34 | if let error = error { 35 | completion(nil, error) 36 | return 37 | } 38 | 39 | guard let data = data, let image = UIImage(data: data) else { 40 | completion(nil, Errors.convertFailed) 41 | return 42 | } 43 | 44 | completion(image, nil) 45 | } 46 | } 47 | task.resume() 48 | return Task(url: url, urlSessionDataTask: task) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /iOSDCRC/Supports/Injectable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Injectable.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/09/10. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol Injectable where Self: UIViewController { 12 | associatedtype Dependency 13 | func inject(dependency: Dependency) 14 | } 15 | -------------------------------------------------------------------------------- /iOSDCRC/Supports/Localizations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Localizations.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/28. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Localizable: RawRepresentable where RawValue == String {} 12 | 13 | extension Localizable { 14 | func localize(tableName: String? = nil, bundle: Bundle = Bundle.main, value: String = "", comment: String = "") -> String { 15 | return NSLocalizedString(rawValue, tableName: tableName, bundle: bundle, value: value, comment: comment) 16 | } 17 | } 18 | 19 | enum Localizations { 20 | enum General: String, Localizable { 21 | case ok = "General.OK" 22 | } 23 | 24 | enum Tweet: String, Localizable { 25 | case seconds = "Tweet.Date.Seconds" 26 | case minutes = "Tweet.Date.Minutes" 27 | case hour = "Tweet.Date.Hour" 28 | case day = "Tweet.Date.Day" 29 | } 30 | 31 | enum Timeline { 32 | enum Alert { 33 | enum AccessTokenGetFailed: String, Localizable { 34 | case title = "Timeline.Alert.TokenGetFailed.Title" 35 | case message = "Timeline.Alert.TokenGetFailed.Message" 36 | } 37 | 38 | enum TweetGetFailed: String, Localizable { 39 | case title = "Timeline.Alert.TweetGetFailed.Title" 40 | case message = "Timeline.Alert.TweetGetFailed.Message" 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/About/AboutViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutViewController.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/26. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class AboutViewController: UITableViewController, AboutViewProtocol, Injectable { 12 | 13 | // MARK: - Dependency 14 | 15 | typealias Dependency = AboutPresenterProtocol 16 | private var presenter: AboutPresenterProtocol! 17 | func inject(dependency: AboutPresenterProtocol) { 18 | presenter = dependency 19 | } 20 | 21 | override func loadView() { 22 | super.loadView() 23 | 24 | title = "About" 25 | 26 | tableView.register(AboutSectionHeaderView.self, forHeaderFooterViewReuseIdentifier: AboutSectionHeaderView.reuseIdentifier) 27 | tableView.register(AboutCell.self, forCellReuseIdentifier: AboutCell.reuseIdentifier) 28 | 29 | presenter.loadAbout() 30 | } 31 | 32 | // MARK: - UITableViewDataSource 33 | 34 | override func numberOfSections(in tableView: UITableView) -> Int { 35 | return presenter.numberOfSections() 36 | } 37 | 38 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 39 | return presenter.numberOfRows(in: section) 40 | } 41 | 42 | override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 43 | let headerView = AboutSectionHeaderView.dequeue(for: tableView) 44 | headerView.titleLabel.text = presenter.section(at: section)?.description 45 | return headerView 46 | } 47 | 48 | override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 49 | return UITableViewAutomaticDimension 50 | } 51 | 52 | override func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat { 53 | return 32 54 | } 55 | 56 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 57 | guard let section = presenter.section(at: indexPath.section) else { 58 | fatalError("section get failed") 59 | } 60 | 61 | let cell = AboutCell.dequeue(for: tableView, at: indexPath) 62 | 63 | switch section { 64 | case .dates: 65 | configure(cell: cell, at: indexPath.row, with: presenter.date(at: indexPath.row)) 66 | case .locations: 67 | configure(cell: cell, at: indexPath.row, with: presenter.location(at: indexPath.row)) 68 | case .sponsors: 69 | configure(cell: cell, at: indexPath.row, with: presenter.sponsor(at: indexPath.row)) 70 | case .speakers: 71 | configure(cell: cell, at: indexPath.row, with: presenter.speaker(at: indexPath.row)) 72 | case .staffs: 73 | configure(cell: cell, at: indexPath.row, with: presenter.staff(at: indexPath.row)) 74 | } 75 | return cell 76 | } 77 | 78 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 79 | return UITableViewAutomaticDimension 80 | } 81 | 82 | override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { 83 | return 44 84 | } 85 | 86 | private func configure(cell: AboutCell, at index: Int, with dateRange: AboutEntity.DateRange?) { 87 | guard let dateRange = dateRange else { 88 | cell.descriptionLabel.text = nil 89 | cell.descriptionLabel.attributedText = nil 90 | return 91 | } 92 | 93 | cell.descriptionLabel.attributedText = nil 94 | cell.descriptionLabel.text = String(format: "%@ - %@", dateRange.from.toString(with: "yyyy/MM/dd HH:mm"), dateRange.to.toString(with: "yyyy/MM/dd HH:mm")) 95 | } 96 | 97 | private func configure(cell: AboutCell, at index: Int, with location: AboutEntity.Location?) { 98 | guard let location = location else { 99 | cell.descriptionLabel.text = nil 100 | cell.descriptionLabel.attributedText = nil 101 | return 102 | } 103 | 104 | cell.descriptionLabel.text = String(format: "%@\n〒%@\n%@", location.name, location.postalCode, location.address) 105 | } 106 | 107 | private func configure(cell: AboutCell, at index: Int, with twitterAccount: AboutEntity.TwitterAccount?) { 108 | guard let twitterAccount = twitterAccount else { 109 | cell.descriptionLabel.text = nil 110 | cell.descriptionLabel.attributedText = nil 111 | return 112 | } 113 | 114 | let attributedText = NSMutableAttributedString(string: twitterAccount.name, attributes: [ 115 | .foregroundColor: UIColor.rc.mainText, 116 | .font: UIFont.systemFont(ofSize: 16, weight: .medium) 117 | ]) 118 | 119 | attributedText.append(NSAttributedString(string: String(format: " %@", twitterAccount.account), attributes: [ 120 | .foregroundColor: UIColor.rc.subText, 121 | .font: UIFont.systemFont(ofSize: 14, weight: .light) 122 | ])) 123 | 124 | cell.descriptionLabel.text = nil 125 | cell.descriptionLabel.attributedText = attributedText 126 | } 127 | 128 | private func configure(cell: AboutCell, at index: Int, with sponsor: AboutEntity.Sponsor?) { 129 | guard let sponsor = sponsor else { 130 | cell.descriptionLabel.text = nil 131 | return 132 | } 133 | 134 | cell.descriptionLabel.text = String(format: "%@(%@)", sponsor.name, sponsor.description) 135 | } 136 | 137 | // MARK: - AboutViewProtocol 138 | 139 | func showAbout() { 140 | tableView.reloadData() 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/About/Entity/AboutEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutEntity.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct AboutEntity: Codable { 12 | let dates: [DateRange] 13 | let locations: [Location] 14 | let sponsors: [Sponsor] 15 | let speakers: [TwitterAccount] 16 | let staffs: [TwitterAccount] 17 | 18 | private enum CodingKeys: String, CodingKey { 19 | case dates 20 | case locations 21 | case sponsors 22 | case speakers 23 | case staffs 24 | } 25 | 26 | struct DateRange: Codable { 27 | let from: Date 28 | let to: Date 29 | 30 | private enum CodingKeys: String, CodingKey { 31 | case from 32 | case to 33 | } 34 | } 35 | 36 | struct Location: Codable { 37 | let name: String 38 | let postalCode: String 39 | let address: String 40 | 41 | private enum CodingKeys: String, CodingKey { 42 | case name 43 | case postalCode = "postal_code" 44 | case address 45 | } 46 | } 47 | 48 | struct Sponsor: Codable { 49 | let name: String 50 | let description: String 51 | 52 | private enum CodingKeys: String, CodingKey { 53 | case name 54 | case description 55 | } 56 | } 57 | 58 | struct TwitterAccount: Codable { 59 | let name: String 60 | let account: String 61 | 62 | private enum CodingKeys: String, CodingKey { 63 | case name 64 | case account 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/About/Entity/AboutSections.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutSections.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum AboutSections: CustomStringConvertible { 12 | case dates 13 | case locations 14 | case sponsors 15 | case speakers 16 | case staffs 17 | 18 | var description: String { 19 | switch self { 20 | case .dates: 21 | return "Date" 22 | case .locations: 23 | return "Locations" 24 | case .sponsors: 25 | return "Sponsors" 26 | case .speakers: 27 | return "Speakers" 28 | case .staffs: 29 | return "Staffs" 30 | } 31 | } 32 | 33 | static let allCases: [AboutSections] = [.dates, .locations, .sponsors, .speakers, .staffs] 34 | } 35 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/About/Interactor/AboutInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutInteractor.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class AboutInteractor: AboutInteractorProtocol { 12 | func fetch(_ completion: @escaping (AboutEntity?) -> ()) { 13 | guard let url = Bundle.main.url(forResource: "about", withExtension: "json") else { 14 | debugPrint(#function, "url get failed") 15 | completion(nil) 16 | return 17 | } 18 | 19 | guard let data = try? Data(contentsOf: url) else { 20 | debugPrint(#function, "data get failed") 21 | completion(nil) 22 | return 23 | } 24 | 25 | let jsonDecoder = JSONDecoder() 26 | jsonDecoder.dateDecodingStrategy = .iso8601 27 | do { 28 | let entity = try jsonDecoder.decode(AboutEntity.self, from: data) 29 | completion(entity) 30 | } catch { 31 | debugPrint(#function, "decode failed", error) 32 | completion(nil) 33 | } 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/About/Interfaces/AboutInterfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutInterfaces.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol AboutInteractorProtocol { 12 | 13 | /// コンテンツを取得する 14 | /// 15 | /// - Parameter completion: (AboutEntity?) -> () 16 | func fetch(_ completion: @escaping (AboutEntity?) -> ()) 17 | } 18 | 19 | protocol AboutPresenterProtocol { 20 | typealias Speaker = AboutEntity.TwitterAccount 21 | typealias Staff = AboutEntity.TwitterAccount 22 | 23 | typealias Dependencies = ( 24 | view: AboutViewProtocol, 25 | interactor: AboutInteractorProtocol 26 | ) 27 | init(dependencies: Dependencies) 28 | 29 | /// コンテンツを取得する 30 | func loadAbout() 31 | 32 | /// セクションの数を返す 33 | /// 34 | /// - Returns: Int 35 | func numberOfSections() -> Int 36 | 37 | /// 該当のセクション情報を返す 38 | /// 39 | /// - Parameter index: Int 40 | /// - Returns: AboutSections? 41 | func section(at index: Int) -> AboutSections? 42 | 43 | /// セクション内のコンテンツの数を返す 44 | /// 45 | /// - Parameter section: Int 46 | /// - Returns: Int 47 | func numberOfRows(in section: Int) -> Int 48 | 49 | /// 日付情報を返す 50 | /// 51 | /// - Parameter index: Int 52 | /// - Returns: AboutEntity.DateRange? 53 | func date(at index: Int) -> AboutEntity.DateRange? 54 | 55 | /// 位置情報を返す 56 | /// 57 | /// - Parameter index: Int 58 | /// - Returns: AboutEntity.Location? 59 | func location(at index: Int) -> AboutEntity.Location? 60 | 61 | /// スタッフの情報を返す 62 | /// 63 | /// - Parameter index: Int 64 | /// - Returns: Staff? 65 | func staff(at index: Int) -> Staff? 66 | 67 | /// 登壇者の情報を返す 68 | /// 69 | /// - Parameter index: Int 70 | /// - Returns: Speaker? 71 | func speaker(at index: Int) -> Speaker? 72 | 73 | /// スポンサーの情報を返す 74 | /// 75 | /// - Parameter index: Int 76 | /// - Returns: AboutEntity.Sponsor? 77 | func sponsor(at index: Int) -> AboutEntity.Sponsor? 78 | } 79 | 80 | protocol AboutViewProtocol: class { 81 | 82 | /// コンテンツを表示する 83 | func showAbout() 84 | } 85 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/About/Presenter/AboutPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutPresenter.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class AboutPresenter: AboutPresenterProtocol { 12 | unowned var view: AboutViewProtocol 13 | var interactor: AboutInteractorProtocol 14 | 15 | required init(dependencies: Dependencies) { 16 | self.view = dependencies.view 17 | self.interactor = dependencies.interactor 18 | } 19 | 20 | var aboutEntity: AboutEntity? 21 | 22 | func loadAbout() { 23 | interactor.fetch { [weak self] (entity) in 24 | self?.aboutEntity = entity 25 | self?.view.showAbout() 26 | } 27 | } 28 | 29 | func numberOfSections() -> Int { 30 | return AboutSections.allCases.count 31 | } 32 | 33 | func section(at index: Int) -> AboutSections? { 34 | guard index < numberOfSections() else { return nil } 35 | return AboutSections.allCases[index] 36 | } 37 | 38 | func numberOfRows(in section: Int) -> Int { 39 | guard let section = self.section(at: section) else { return 0 } 40 | switch section { 41 | case .dates: 42 | return aboutEntity?.dates.count ?? 0 43 | case .locations: 44 | return aboutEntity?.locations.count ?? 0 45 | case .sponsors: 46 | return aboutEntity?.sponsors.count ?? 0 47 | case .speakers: 48 | return aboutEntity?.speakers.count ?? 0 49 | case .staffs: 50 | return aboutEntity?.staffs.count ?? 0 51 | } 52 | } 53 | 54 | func date(at index: Int) -> AboutEntity.DateRange? { 55 | guard let dates = aboutEntity?.dates, index < dates.count else { return nil } 56 | return dates[index] 57 | } 58 | 59 | func location(at index: Int) -> AboutEntity.Location? { 60 | guard let locations = aboutEntity?.locations, index < locations.count else { return nil } 61 | return locations[index] 62 | } 63 | 64 | func sponsor(at index: Int) -> AboutEntity.Sponsor? { 65 | guard let sponsors = aboutEntity?.sponsors, index < sponsors.count else { return nil } 66 | return sponsors[index] 67 | } 68 | 69 | func speaker(at index: Int) -> Speaker? { 70 | guard let speakers = aboutEntity?.speakers, index < speakers.count else { return nil } 71 | return speakers[index] 72 | } 73 | 74 | func staff(at index: Int) -> Staff? { 75 | guard let staffs = aboutEntity?.staffs, index < staffs.count else { return nil } 76 | return staffs[index] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/About/Views/AboutCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutCell.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class AboutCell: UITableViewCell, ReusableTableViewCell { 12 | 13 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 14 | super.init(style: style, reuseIdentifier: reuseIdentifier) 15 | 16 | setUp() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | 22 | setUp() 23 | } 24 | 25 | private lazy var setUp: () -> () = { 26 | selectionStyle = .none 27 | 28 | contentView.addSubview(descriptionLabel, constraints: [ 29 | descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), 30 | descriptionLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), 31 | contentView.trailingAnchor.constraint(greaterThanOrEqualTo: descriptionLabel.trailingAnchor, constant: 16), 32 | contentView.bottomAnchor.constraint(greaterThanOrEqualTo: descriptionLabel.bottomAnchor, constant: 16) 33 | ]) 34 | 35 | return {} 36 | }() 37 | 38 | lazy var descriptionLabel: UILabel = { 39 | let label = UILabel() 40 | label.font = UIFont.systemFont(ofSize: 14, weight: .medium) 41 | label.textColor = UIColor.rc.mainText 42 | label.numberOfLines = 0 43 | label.lineBreakMode = .byWordWrapping 44 | label.setContentHuggingPriority(.defaultLow, for: .horizontal) 45 | label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 46 | return label 47 | }() 48 | } 49 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/About/Views/AboutSectionHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutSectionHeaderView.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class AboutSectionHeaderView: UITableViewHeaderFooterView, ReusableTableViewHeaderFooterView { 12 | 13 | override init(reuseIdentifier: String?) { 14 | super.init(reuseIdentifier: reuseIdentifier) 15 | 16 | setUp() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | 22 | setUp() 23 | } 24 | 25 | private lazy var setUp: () -> () = { 26 | contentView.addSubview(titleLabel, constraints: [ 27 | titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), 28 | titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), 29 | contentView.trailingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 16), 30 | contentView.bottomAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: 8), 31 | ]) 32 | 33 | return {} 34 | }() 35 | 36 | lazy var titleLabel: UILabel = { 37 | let label = UILabel() 38 | label.font = UIFont.systemFont(ofSize: 16, weight: .heavy) 39 | label.textColor = UIColor.rc.mainText 40 | label.numberOfLines = 0 41 | label.lineBreakMode = .byTruncatingTail 42 | label.setContentHuggingPriority(.defaultLow, for: .horizontal) 43 | label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 44 | return label 45 | }() 46 | 47 | } 48 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Root/Entity/RootEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootEntity.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum RootEntity { 12 | case about 13 | case timetable 14 | case timeline 15 | 16 | func toString() -> String { 17 | switch self { 18 | case .about: 19 | return "About" 20 | case .timetable: 21 | return "Timetable" 22 | case .timeline: 23 | return "Timeline" 24 | } 25 | } 26 | 27 | static let allCases: [RootEntity] = [.about, .timetable, .timeline] 28 | } 29 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Root/Interactor/RootInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootInteractor.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RootInteractor: RootInteractorProtocol { 12 | func menu(completion: @escaping ([RootEntity]) -> ()) { 13 | completion(RootEntity.allCases) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Root/Interface/RootInterfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootInterface.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol RootInteractorProtocol { 12 | 13 | /// メニューを取得する 14 | /// 15 | /// - Parameter completion: ([RootEntity]) -> () 16 | func menu(completion: @escaping ([RootEntity]) -> ()) 17 | } 18 | 19 | protocol RootPresenterProtocol: class { 20 | typealias Menu = RootEntity 21 | 22 | typealias Dependencies = ( 23 | view: RootViewProtocol, 24 | interactor: RootInteractorProtocol, 25 | router: RootWireframeProtocol 26 | ) 27 | 28 | init(dependencies: Dependencies) 29 | 30 | /// メニューを取得する 31 | func loadMenu() 32 | 33 | /// メニューの数を返す 34 | /// 35 | /// - Returns: Int 36 | func numberOfMenus() -> Int 37 | 38 | /// 該当するメニューを返す 39 | /// 40 | /// - Parameter index: Int 41 | /// - Returns: Menu? 42 | func menu(at index: Int) -> Menu? 43 | 44 | /// 該当のメニューを選択する 45 | /// 46 | /// - Parameter index: Int 47 | func select(at index: Int) 48 | } 49 | 50 | protocol RootWireframeProtocol { 51 | init(viewController: UIViewController) 52 | 53 | /// Aboutページへ遷移する 54 | func about() 55 | 56 | /// Timelineページへ遷移する 57 | func timeline() 58 | 59 | /// Timetableページへ遷移する 60 | func timetable() 61 | } 62 | 63 | protocol RootViewProtocol: class { 64 | 65 | /// メニューを表示する 66 | func showMenu() 67 | } 68 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Root/Presenter/RootPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootPresenter.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RootPresenter: RootPresenterProtocol { 12 | unowned var view: RootViewProtocol 13 | var interactor: RootInteractorProtocol 14 | var router: RootWireframeProtocol 15 | required init(dependencies: Dependencies) { 16 | self.view = dependencies.view 17 | self.interactor = dependencies.interactor 18 | self.router = dependencies.router 19 | } 20 | 21 | var menuList: [Menu] = [] 22 | 23 | func loadMenu() { 24 | interactor.menu { [weak self] (menuList) in 25 | self?.menuList = menuList 26 | self?.view.showMenu() 27 | } 28 | } 29 | 30 | func numberOfMenus() -> Int { 31 | return menuList.count 32 | } 33 | 34 | func menu(at index: Int) -> Menu? { 35 | guard index < numberOfMenus() else { return nil } 36 | return menuList[index] 37 | } 38 | 39 | func select(at index: Int) { 40 | guard let menu = self.menu(at: index) else { return } 41 | 42 | switch menu { 43 | case .about: 44 | router.about() 45 | case .timetable: 46 | router.timetable() 47 | case .timeline: 48 | router.timeline() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Root/RootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// トップページ 12 | final class RootViewController: UITableViewController, RootViewProtocol, Injectable { 13 | 14 | private let reuseIdentifier: String = "Cell" 15 | 16 | // MARK: - Dependency 17 | 18 | typealias Dependency = RootPresenterProtocol 19 | private var presenter: RootPresenterProtocol! 20 | func inject(dependency: RootPresenterProtocol) { 21 | presenter = dependency 22 | } 23 | 24 | // MARK: - lifecycle 25 | 26 | override func loadView() { 27 | super.loadView() 28 | 29 | title = "iOSDCRC 2018" 30 | tableView.tableHeaderView = logoView 31 | tableView.tableFooterView = footerView 32 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier) 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | presenter.loadMenu() 39 | } 40 | 41 | override func viewWillAppear(_ animated: Bool) { 42 | super.viewWillAppear(animated) 43 | 44 | if let indexPathForSelectedRow = tableView.indexPathForSelectedRow { 45 | tableView.deselectRow(at: indexPathForSelectedRow, animated: true) 46 | } 47 | } 48 | 49 | override func numberOfSections(in tableView: UITableView) -> Int { 50 | return 1 51 | } 52 | 53 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | return presenter.numberOfMenus() 55 | } 56 | 57 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 58 | let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) 59 | cell.accessoryType = .disclosureIndicator 60 | cell.textLabel?.text = presenter.menu(at: indexPath.row)?.toString() 61 | return cell 62 | } 63 | 64 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 65 | presenter.select(at: indexPath.row) 66 | } 67 | 68 | // MARK: - UI 69 | 70 | lazy var logoView: RootLogoView = RootLogoView(frame: CGRect(origin: .zero, size: CGSize(width: self.view.frame.size.width, height: 200))) 71 | 72 | lazy var footerView: RootFooterView = { 73 | let footerView = RootFooterView(frame: CGRect(origin: .zero, size: CGSize(width: self.view.frame.size.width, height: 44))) 74 | footerView.copyLabel.text = "(C) iOSDC Reject Conference 2018" 75 | return footerView 76 | }() 77 | 78 | // MARK: - RootViewProtocol 79 | 80 | func showMenu() { 81 | tableView.reloadData() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Root/Router/RootWireframe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootWireframe.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RootWireframe: RootWireframeProtocol { 12 | unowned var viewController: UIViewController 13 | required init(viewController: UIViewController) { 14 | self.viewController = viewController 15 | } 16 | 17 | func about() { 18 | let aboutViewController = AboutViewController() 19 | 20 | let view = aboutViewController 21 | let interactor = AboutInteractor() 22 | 23 | let presenter = AboutPresenter(dependencies: (view: view, interactor: interactor)) 24 | aboutViewController.inject(dependency: presenter) 25 | viewController.navigationController?.pushViewController(aboutViewController, animated: true) 26 | } 27 | 28 | func timeline() { 29 | let timelineViewController = TimelineViewController() 30 | 31 | let view = timelineViewController 32 | let interacotor = TimelineInteractor() 33 | 34 | let presenter = TimelinePresenter(dependencies: (view: view, interactor: interacotor)) 35 | timelineViewController.inject(dependency: presenter) 36 | viewController.navigationController?.pushViewController(timelineViewController, animated: true) 37 | } 38 | 39 | func timetable() { 40 | let timetableViewController = TimetableViewController() 41 | 42 | let view = timetableViewController 43 | let interactor = TimetableInteractor() 44 | 45 | let presenter = TimetablePresenter(dependencies: (view: view, interactor: interactor)) 46 | timetableViewController.inject(dependency: presenter) 47 | viewController.navigationController?.pushViewController(timetableViewController, animated: true) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Root/View/RootFooterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootFooterView.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class RootFooterView: UIView { 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | setUp() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | 22 | setUp() 23 | } 24 | 25 | private lazy var setUp: () -> () = { 26 | addSubview(copyLabel, constraints: [ 27 | copyLabel.centerXAnchor.constraint(equalTo: centerXAnchor), 28 | copyLabel.centerYAnchor.constraint(equalTo: centerYAnchor), 29 | ]) 30 | 31 | return {} 32 | }() 33 | 34 | lazy var copyLabel: UILabel = { 35 | let label = UILabel() 36 | label.textAlignment = .center 37 | label.textColor = UIColor.rc.mainText 38 | label.font = UIFont.systemFont(ofSize: 14, weight: .medium) 39 | return label 40 | }() 41 | 42 | } 43 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Root/View/RootLogoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootLogoView.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class RootLogoView: UIView { 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | 15 | setUp() 16 | } 17 | 18 | required init?(coder aDecoder: NSCoder) { 19 | super.init(coder: aDecoder) 20 | 21 | setUp() 22 | } 23 | 24 | private lazy var setUp: () -> () = { 25 | addSubview(logoImageView, constraints: [ 26 | logoImageView.centerXAnchor.constraint(equalTo: centerXAnchor), 27 | logoImageView.centerYAnchor.constraint(equalTo: centerYAnchor), 28 | ]) 29 | 30 | return {} 31 | }() 32 | 33 | lazy var logoImageView: UIImageView = UIImageView(image: #imageLiteral(resourceName: "logo")) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timeline/Interactor/TimelineInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineInteractor.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import API 11 | 12 | class TimelineInteractor: TimelineInteractorProtocol { 13 | func token(with consumerKey: String, and consumerSecret: String, completion: @escaping (String?) -> ()) { 14 | let token = Twitter.Request.Token(consumerKey: consumerKey, consumerSecret: consumerSecret) 15 | Session.json(with: token, and: Twitter.Response.Token.self) { (result, error) in 16 | guard let result = result else { 17 | completion(nil) 18 | return 19 | } 20 | 21 | completion(result.accessToken) 22 | } 23 | } 24 | 25 | func search(with accessToken: String, andKeyword keyword: String, count: Int = 100, sinceID: Int64? = nil, maxID: Int64? = nil, completion: @escaping ([Twitter.Response.Status]) -> ()) { 26 | let search = Twitter.Request.Search(accessToken: accessToken, q: keyword, count: count, sinceID: sinceID, maxID: maxID) 27 | Session.dateDecodingStrategy = Twitter.Response.dateDecodingStrategy 28 | Session.json(with: search, and: Twitter.Response.Statuses.self) { (result, error) in 29 | guard let result = result else { 30 | debugPrint(#function, "error", error) 31 | completion([]) 32 | return 33 | } 34 | 35 | completion(result.statuses) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timeline/Interface/TimelineInterfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineInterfaces.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol TimelineInteractorProtocol { 12 | 13 | /// アクセストークンを取得する 14 | /// 15 | /// - Parameters: 16 | /// - consumerKey: Twitterのconsumer_key 17 | /// - consumerSecret: Twitterのconsumer_secret 18 | /// - completion: (accessToken: String?) -> () 19 | func token(with consumerKey: String, and consumerSecret: String, completion: @escaping (String?) -> ()) 20 | 21 | /// 検索を実行する 22 | /// 23 | /// - Parameters: 24 | /// - accessToken: アクセストークン 25 | /// - keyword: 検索ワード 26 | /// - count: 件数 27 | /// - sinceID: 取得する最小値 28 | /// - maxID: 取得する最大値 29 | /// - completion: ([Twitter.Response.Status]) -> () 30 | func search(with accessToken: String, andKeyword keyword: String, count: Int, sinceID: Int64?, maxID: Int64?, completion: @escaping ([Twitter.Response.Status]) -> ()) 31 | } 32 | 33 | protocol TimelinePresenterProtocol: class { 34 | typealias Tweet = Twitter.Response.Status 35 | 36 | typealias Dependencies = ( 37 | view: TimelineViewProtocol, 38 | interactor: TimelineInteractorProtocol 39 | ) 40 | init(dependencies: Dependencies) 41 | 42 | /// タイムラインを読み込む 43 | func loadTimeline() 44 | 45 | /// 新しいツイートを検索する 46 | func findNewTweets() 47 | 48 | /// 古いツイートを検索する 49 | func findOldTweets() 50 | 51 | /// 画面が表示されるときに呼ばれる 52 | func viewWillShow() 53 | 54 | /// 画面が非表示になるときに呼ばれる 55 | func viewWillHide() 56 | 57 | /// ツイートの件数を返す 58 | /// 59 | /// - Returns: Int 60 | func numberOfTweets() -> Int 61 | 62 | /// 該当のツイートを返す 63 | /// 64 | /// - Parameter index: Int 65 | /// - Returns: Tweet? 66 | func tweet(at index: Int) -> Tweet? 67 | } 68 | 69 | protocol TimelineViewProtocol: class { 70 | 71 | /// 画面全体のローディングを表示する 72 | func showLoading() 73 | 74 | /// 画面全体のローディングを非表示にする 75 | func hideLoading() 76 | 77 | /// 古いツイートを読み込む時のローディングを表示する 78 | func showLoadingBottom() 79 | 80 | /// 古いツイートを読み込む時のローディングを非表示する 81 | func hideLoadingBottom() 82 | 83 | /// タイムラインを表示する 84 | func showTimeline() 85 | 86 | /// タイムラインに追加する 87 | /// 88 | /// - Parameter indexes: [Int] 89 | func addTimeline(at indexes: [Int]) 90 | 91 | /// 日付を更新する 92 | func updateDate() 93 | 94 | /// アクセストークンを取得するのに失敗したアラートを表示する 95 | func showAlertTokenGetFailed() 96 | 97 | /// タイムラインを取得するのに失敗したアラートを表示する 98 | func showAlertTimelineGetFailed() 99 | } 100 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timeline/Presenter/TimelinePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelinePresenter.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TimelinePresenter: NSObject, TimelinePresenterProtocol { 12 | unowned var view: TimelineViewProtocol 13 | var interactor: TimelineInteractorProtocol 14 | 15 | required init(dependencies: Dependencies) { 16 | self.view = dependencies.view 17 | self.interactor = dependencies.interactor 18 | 19 | super.init() 20 | 21 | NotificationCenter.default.addObserver(self, selector: #selector(timerActivateIfNeeded(with:)), name: .UIApplicationDidBecomeActive, object: nil) 22 | NotificationCenter.default.addObserver(self, selector: #selector(timerInactivateIfNeeded(with:)), name: Notification.Name.UIApplicationDidEnterBackground, object: nil) 23 | } 24 | 25 | let keyword = "#iosdcrc" 26 | 27 | let numberOfTweet: Int = 30 28 | 29 | let timeInterval: TimeInterval = 30 30 | 31 | var accessToken: String? 32 | 33 | var isLoading: Bool = false 34 | 35 | func loadTimeline() { 36 | guard !isLoading else { return } 37 | 38 | isLoading = true 39 | interactor.token(with: Constants.Twitter.consumerKey, and: Constants.Twitter.consumerSecret) { [weak self] (accessToken) in 40 | guard let accessToken = accessToken else { 41 | self?.view.showAlertTokenGetFailed() 42 | return 43 | } 44 | self?.isLoading = false 45 | self?.accessToken = accessToken 46 | self?.fetch() 47 | } 48 | } 49 | 50 | func viewWillShow() { 51 | timerActivateIfNeeded(with: nil) 52 | } 53 | 54 | func viewWillHide() { 55 | timerInactivateIfNeeded(with: nil) 56 | } 57 | 58 | var timer: Timer? 59 | 60 | @objc func timerActivateIfNeeded(with notification: Notification?) { 61 | if let timer = timer, timer.isValid { 62 | return 63 | } 64 | 65 | timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: true) 66 | } 67 | 68 | @objc func timerInactivateIfNeeded(with notification: Notification?) { 69 | timer?.invalidate() 70 | } 71 | 72 | @objc private func handleTimer() { 73 | findNewTweets() 74 | } 75 | 76 | var tweets: [Tweet] = [] 77 | 78 | func fetch() { 79 | guard let accessToken = accessToken, !isLoading else { 80 | return 81 | } 82 | 83 | isLoading = true 84 | view.showLoading() 85 | interactor.search(with: accessToken, andKeyword: keyword, count: numberOfTweet, sinceID: nil, maxID: nil, completion: { [weak self] tweets in 86 | self?.tweets = tweets 87 | self?.view.showTimeline() 88 | self?.view.hideLoading() 89 | self?.isLoading = false 90 | }) 91 | } 92 | 93 | func findNewTweets() { 94 | guard let accessToken = accessToken, let sinceID = tweets.map({ $0.id }).max(), !isLoading else { 95 | return 96 | } 97 | 98 | isLoading = true 99 | interactor.search(with: accessToken, andKeyword: keyword, count: numberOfTweet, sinceID: sinceID, maxID: nil) { [weak self] (tweets) in 100 | self?.tweets.insert(contentsOf: tweets, at: 0) 101 | self?.view.addTimeline(at: (0.. Int { 131 | return tweets.count 132 | } 133 | 134 | func tweet(at index: Int) -> TimelinePresenterProtocol.Tweet? { 135 | guard index < numberOfTweets() else { return nil } 136 | return tweets[index] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timeline/TimelineViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineViewController.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Twitterのタイムラインを表示 12 | class TimelineViewController: UITableViewController, TimelineViewProtocol, Injectable { 13 | 14 | // MARK: - Dependency 15 | 16 | typealias Dependency = TimelinePresenterProtocol 17 | private var presenter: TimelinePresenterProtocol! 18 | func inject(dependency: TimelinePresenterProtocol) { 19 | presenter = dependency 20 | } 21 | 22 | // MARK: - lifecycle 23 | 24 | override func loadView() { 25 | super.loadView() 26 | 27 | title = "Timeline" 28 | 29 | tableView.register(TimelineCell.self, forCellReuseIdentifier: TimelineCell.reuseIdentifier) 30 | 31 | view.addSubview(loadingView, constraints: [ 32 | loadingView.widthAnchor.constraint(equalTo: view.widthAnchor), 33 | loadingView.heightAnchor.constraint(equalTo: view.heightAnchor), 34 | loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 35 | loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor), 36 | ]) 37 | } 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | 42 | presenter.loadTimeline() 43 | } 44 | 45 | override func viewWillAppear(_ animated: Bool) { 46 | super.viewWillAppear(animated) 47 | 48 | presenter.viewWillShow() 49 | } 50 | 51 | override func viewWillDisappear(_ animated: Bool) { 52 | super.viewWillDisappear(animated) 53 | 54 | presenter.viewWillHide() 55 | } 56 | 57 | // MARK: - UI 58 | 59 | lazy var loadingView = LoadingView() 60 | 61 | lazy var footerLoadingView: LoadingView = LoadingView(frame: CGRect(origin: .zero, size: CGSize(width: self.view.bounds.size.width, height: 44))) 62 | 63 | // MARK: - TimelineViewProtocol 64 | 65 | func showLoading() { 66 | loadingView.startAnimating() 67 | } 68 | 69 | func hideLoading() { 70 | loadingView.stopAnimating() 71 | } 72 | 73 | func showLoadingBottom() { 74 | tableView.tableFooterView = footerLoadingView 75 | footerLoadingView.startAnimating() 76 | } 77 | 78 | func hideLoadingBottom() { 79 | footerLoadingView.stopAnimating() 80 | tableView.tableFooterView = nil 81 | } 82 | 83 | func showTimeline() { 84 | tableView.reloadData() 85 | } 86 | 87 | func addTimeline(at indexes: [Int]) { 88 | let indexPathes = indexes.map { IndexPath(row: $0, section: 0) } 89 | if indexes.contains(0) { 90 | tableView.insertRows(at: indexPathes, with: .automatic) 91 | } else { 92 | tableView.insertRows(at: indexPathes, with: .bottom) 93 | } 94 | } 95 | 96 | func updateDate() { 97 | (0.. Int { 119 | return 1 120 | } 121 | 122 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 123 | return presenter.numberOfTweets() 124 | } 125 | 126 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 127 | let cell = TimelineCell.dequeue(for: tableView, at: indexPath) 128 | if let tweet = presenter.tweet(at: indexPath.row) { 129 | configure(cell, and: tweet) 130 | } 131 | return cell 132 | } 133 | 134 | private func configure(_ cell: TimelineCell, and tweet: Twitter.Response.Status) { 135 | let name: NSMutableAttributedString = NSMutableAttributedString(string: tweet.user.name, attributes: [ 136 | .foregroundColor: UIColor.rc.mainText, 137 | .font: UIFont.systemFont(ofSize: 16, weight: .medium) 138 | ]) 139 | 140 | name.append(NSAttributedString(string: String(format: " @%@", tweet.user.screenName), attributes: [ 141 | .foregroundColor: UIColor.rc.subText, 142 | .font: UIFont.systemFont(ofSize: 14, weight: .light) 143 | ])) 144 | 145 | cell.nameLabel.attributedText = name 146 | 147 | let style = NSMutableParagraphStyle() 148 | style.lineSpacing = 4.0 149 | cell.tweetLabel.attributedText = NSAttributedString(string: tweet.text, attributes: [ 150 | .paragraphStyle: style, 151 | .font: UIFont.systemFont(ofSize: 14, weight: .light), 152 | .foregroundColor: UIColor.rc.mainText 153 | ]) 154 | cell.dateLabel.text = TweetDateConverter.convert(tweet.createdAt) 155 | } 156 | 157 | override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 158 | guard let cell = cell as? TimelineCell, let tweet = presenter.tweet(at: indexPath.row) else { return } 159 | 160 | cell.iconImageView.image = nil 161 | cell.task = ImageLoader.load(with: tweet.user.profileImageUrlHttps, completion: { (image, error) in 162 | guard let image = image, cell.task?.url == tweet.user.profileImageUrlHttps else { return } 163 | cell.iconImageView.image = image 164 | }) 165 | } 166 | 167 | override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { 168 | guard let cell = cell as? TimelineCell else { return } 169 | cell.task?.cancel() 170 | } 171 | 172 | // MARK: - UITableViewDelegate 173 | 174 | override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { 175 | return 80 176 | } 177 | 178 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 179 | return UITableViewAutomaticDimension 180 | } 181 | 182 | // MARK: - UIScrollViewDelegate 183 | 184 | override func scrollViewDidScroll(_ scrollView: UIScrollView) { 185 | guard scrollView.contentSize.height - (scrollView.contentOffset.y + scrollView.frame.size.height) < 200.0 else { return } 186 | presenter.findOldTweets() 187 | } 188 | } 189 | 190 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timeline/View/TimelineCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineCell.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/23. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class TimelineCell: UITableViewCell, ReusableTableViewCell { 12 | 13 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 14 | super.init(style: style, reuseIdentifier: reuseIdentifier) 15 | 16 | setUp() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | 22 | setUp() 23 | } 24 | 25 | private lazy var setUp: () -> () = { 26 | selectionStyle = .none 27 | 28 | contentView.addSubview(iconImageView, constraints: [ 29 | iconImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), 30 | iconImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), 31 | iconImageView.widthAnchor.constraint(equalToConstant: 48).priority(.required), 32 | iconImageView.heightAnchor.constraint(equalToConstant: 48).priority(.required), 33 | contentView.bottomAnchor.constraint(greaterThanOrEqualTo: iconImageView.bottomAnchor, constant: 8) 34 | ]) 35 | 36 | contentView.addSubview(dateLabel, constraints: [ 37 | contentView.trailingAnchor.constraint(greaterThanOrEqualTo: dateLabel.trailingAnchor, constant: 16), 38 | dateLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), 39 | ]) 40 | 41 | contentView.addSubview(nameLabel, constraints: [ 42 | nameLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), 43 | nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), 44 | dateLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 8) 45 | ]) 46 | 47 | contentView.addSubview(tweetLabel, constraints: [ 48 | tweetLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), 49 | tweetLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8), 50 | contentView.trailingAnchor.constraint(greaterThanOrEqualTo: tweetLabel.trailingAnchor, constant: 16), 51 | contentView.bottomAnchor.constraint(greaterThanOrEqualTo: tweetLabel.bottomAnchor, constant: 8) 52 | ]) 53 | 54 | return {} 55 | }() 56 | 57 | // MARK: - Elements 58 | 59 | var task: ImageLoader.Task? 60 | 61 | // MARK: - UI 62 | 63 | lazy var iconImageView: UIImageView = { 64 | let imageView = UIImageView() 65 | imageView.contentMode = .scaleAspectFill 66 | imageView.clipsToBounds = true 67 | imageView.layer.cornerRadius = 24 68 | imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 69 | imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) 70 | imageView.setContentHuggingPriority(.defaultLow, for: .horizontal) 71 | imageView.setContentHuggingPriority(.defaultLow, for: .vertical) 72 | return imageView 73 | }() 74 | 75 | lazy var nameLabel: UILabel = { 76 | let label = UILabel() 77 | label.lineBreakMode = .byTruncatingTail 78 | label.numberOfLines = 1 79 | label.setContentHuggingPriority(.defaultLow, for: .horizontal) 80 | label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 81 | return label 82 | }() 83 | 84 | lazy var dateLabel: UILabel = { 85 | let label = UILabel() 86 | label.textAlignment = .right 87 | label.numberOfLines = 1 88 | label.font = UIFont.systemFont(ofSize: 14, weight: .light) 89 | label.textColor = UIColor.rc.subText 90 | return label 91 | }() 92 | 93 | lazy var tweetLabel: UILabel = { 94 | let label = UILabel() 95 | label.numberOfLines = 0 96 | label.lineBreakMode = .byWordWrapping 97 | label.font = UIFont.systemFont(ofSize: 14, weight: .medium) 98 | label.textColor = UIColor.rc.mainText 99 | return label 100 | }() 101 | } 102 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timeline/View/TweetDateConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetDateConverter.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/23. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TweetDateConverter { 12 | static func convert(_ date: Date, with now: Date = Date()) -> String { 13 | let diffTimeInterval = now.timeIntervalSince1970 - date.timeIntervalSince1970 14 | 15 | let day = Int(diffTimeInterval / Constants.Date.day) 16 | let hour = Int(diffTimeInterval / Constants.Date.hour) 17 | let minutes = Int(diffTimeInterval / Constants.Date.minutes) 18 | let sec = Int(diffTimeInterval) 19 | 20 | if 7 < day { 21 | let dateFormatter = DateFormatter() 22 | dateFormatter.timeStyle = .short 23 | return dateFormatter.string(from: date) 24 | } else if 0 < day { 25 | return String(format: "%d%@", day, NSLocalizedString("Tweet.Date.Day", comment: "d")) 26 | } else if 0 < hour { 27 | return String(format: "%d%@", hour, NSLocalizedString("Tweet.Date.Hour", comment: "h")) 28 | } else if 0 < minutes { 29 | return String(format: "%d%@", minutes, NSLocalizedString("Tweet.Date.Minutes", comment: "m")) 30 | } else { 31 | return String(format: "%d%@", sec, NSLocalizedString("Tweet.Date.Seconds", comment: "s")) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timetable/Entity/TimetableEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableEntity.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TimetableView 11 | 12 | struct Event: Codable, Equatable { 13 | let title: String 14 | let description: String 15 | let startAt: Date 16 | let endAt: Date 17 | let tracks: [Track] 18 | 19 | private enum CodingKeys: String, CodingKey { 20 | case title 21 | case description 22 | case startAt = "start_at" 23 | case endAt = "end_at" 24 | case tracks 25 | } 26 | 27 | struct Track: Codable, Equatable { 28 | let name: String 29 | let sessions: [Session] 30 | 31 | private enum CodingKeys: String, CodingKey { 32 | case name 33 | case sessions 34 | } 35 | 36 | struct Session: Codable, Equatable { 37 | let title: String 38 | let description: String 39 | let author: String? 40 | let url: URL? 41 | let startAt: Date 42 | let endAt: Date 43 | 44 | private enum CodingKeys: String, CodingKey { 45 | case title 46 | case description 47 | case author 48 | case url 49 | case startAt = "start_at" 50 | case endAt = "end_at" 51 | } 52 | } 53 | } 54 | } 55 | 56 | extension Event.Track.Session: TimetableViewItem {} 57 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timetable/Interactor/TimetableInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableInteractor.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TimetableInteractor: TimetableInteractorProtocol { 12 | func events(_ completion: @escaping ([Event]) -> ()) { 13 | guard let path = Bundle.main.path(forResource: "iosdcrc", ofType: "json") else { 14 | completion([]) 15 | return 16 | } 17 | 18 | let fileManager = FileManager.default 19 | 20 | guard let data = fileManager.contents(atPath: path) else { 21 | completion([]) 22 | return 23 | } 24 | 25 | let jsonDecoder = JSONDecoder() 26 | jsonDecoder.dateDecodingStrategy = .iso8601 27 | do { 28 | let events = try jsonDecoder.decode([Event].self, from: data) 29 | completion(events) 30 | } catch { 31 | completion([]) 32 | return 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timetable/Interface/TimetableInterfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableInterfaces.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/27. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol TimetableInteractorProtocol { 12 | 13 | /// イベントを取得する 14 | /// 15 | /// - Parameter completion: ([Event]) -> () 16 | func events(_ completion: @escaping ([Event]) -> ()) 17 | } 18 | 19 | protocol TimetablePresenterProtocol: class { 20 | typealias Dependencies = ( 21 | view: TimetableViewpProtocol, 22 | interactor: TimetableInteractorProtocol 23 | ) 24 | init(dependencies: Dependencies) 25 | 26 | /// イベントを取得する 27 | func loadEvents() 28 | 29 | /// イベントの件数を返す 30 | /// 31 | /// - Returns: Int 32 | func numberOfEvents() -> Int 33 | 34 | /// 該当のイベントを返す 35 | /// 36 | /// - Parameter index: Int 37 | /// - Returns: Event? 38 | func event(at index: Int) -> Event? 39 | 40 | /// 現在設定中のイベントを返す 41 | var currentEvent: Event? { get } 42 | 43 | /// イベントを選択する 44 | /// 45 | /// - Parameter index: Int 46 | func selectEvent(at index: Int) 47 | } 48 | 49 | protocol TimetableViewpProtocol: class { 50 | 51 | /// イベントを表示する 52 | func showEvents() 53 | 54 | /// イベントを更新する 55 | func updateEvent() 56 | } 57 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timetable/Presenter/TimetablePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetablePresenter.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TimetablePresenter: TimetablePresenterProtocol { 12 | unowned var view: TimetableViewpProtocol 13 | var interactor: TimetableInteractorProtocol 14 | required init(dependencies: Dependencies) { 15 | self.view = dependencies.view 16 | self.interactor = dependencies.interactor 17 | } 18 | 19 | var events: [Event] = [] 20 | 21 | var currentEvent: Event? 22 | 23 | func loadEvents() { 24 | interactor.events { [weak self] (events) in 25 | self?.events = events 26 | // TODO: implove some logic 27 | self?.currentEvent = events.first 28 | self?.view.showEvents() 29 | self?.view.updateEvent() 30 | } 31 | } 32 | 33 | func numberOfEvents() -> Int { 34 | return events.count 35 | } 36 | 37 | func event(at index: Int) -> Event? { 38 | guard index < numberOfEvents() else { return nil } 39 | return events[index] 40 | } 41 | 42 | func selectEvent(at index: Int) { 43 | guard let event = event(at: index) else { return } 44 | 45 | currentEvent = event 46 | 47 | view.updateEvent() 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timetable/TimetableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableViewController.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TimetableView 11 | 12 | /// タイムテーブルを表示する 13 | final class TimetableViewController: UIViewController, TimetableViewDataSource, UICollectionViewDelegate, UIScrollViewDelegate, TimetableViewpProtocol, Injectable { 14 | 15 | // MARK: - Dependency 16 | 17 | typealias Dependency = TimetablePresenterProtocol 18 | private var presenter: TimetablePresenterProtocol! 19 | func inject(dependency: TimetablePresenterProtocol) { 20 | presenter = dependency 21 | } 22 | 23 | // MARK: - Constants 24 | 25 | let heightOfHour: CGFloat = 800 26 | 27 | let itemWidth: CGFloat = 240 28 | 29 | // MARK: - lifecycle 30 | 31 | override func loadView() { 32 | super.loadView() 33 | 34 | title = "Timetable" 35 | automaticallyAdjustsScrollViewInsets = false 36 | 37 | view.backgroundColor = .white 38 | view.addSubview(segmentedControl, constraints: [ 39 | segmentedControlTopConstraint, 40 | segmentedControl.leadingAnchor.constraint(equalTo: view.safeArea.leadingAnchor, constant: 16), 41 | view.safeArea.trailingAnchor.constraint(equalTo: segmentedControl.trailingAnchor, constant: 16), 42 | segmentedControl.heightAnchor.constraint(equalToConstant: 24), 43 | ]) 44 | 45 | view.addSubview(tracksScrollView, constraints: [ 46 | tracksScrollView.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 8), 47 | tracksScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 60), 48 | tracksScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 49 | tracksScrollView.heightAnchor.constraint(equalToConstant: 44) 50 | ]) 51 | 52 | view.addSubview(timesScrollView, constraints: [ 53 | timesScrollView.topAnchor.constraint(equalTo: tracksScrollView.bottomAnchor), 54 | timesScrollView.leadingAnchor.constraint(equalTo: view.safeArea.leadingAnchor), 55 | timesScrollView.widthAnchor.constraint(equalToConstant: 60), 56 | timesScrollView.bottomAnchor.constraint(equalTo: view.safeArea.bottomAnchor) 57 | ]) 58 | 59 | view.addSubview(timetableView, constraints: [ 60 | timetableView.leadingAnchor.constraint(equalTo: timesScrollView.trailingAnchor), 61 | timetableView.topAnchor.constraint(equalTo: tracksScrollView.bottomAnchor), 62 | timetableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 63 | timetableView.bottomAnchor.constraint(equalTo: view.safeArea.bottomAnchor) 64 | ]) 65 | } 66 | 67 | override func viewDidLoad() { 68 | super.viewDidLoad() 69 | 70 | presenter.loadEvents() 71 | } 72 | 73 | override func didReceiveMemoryWarning() { 74 | super.didReceiveMemoryWarning() 75 | // Dispose of any resources that can be recreated. 76 | } 77 | 78 | // MARK: - UI 79 | 80 | lazy var segmentedControl: UISegmentedControl = { 81 | let segmentedControl = UISegmentedControl() 82 | segmentedControl.tintColor = UIColor.rc.mainText 83 | segmentedControl.backgroundColor = .white 84 | segmentedControl.addTarget(self, action: #selector(onChange(segmentedControl:)), for: .valueChanged) 85 | return segmentedControl 86 | }() 87 | 88 | @objc private func onChange(segmentedControl: UISegmentedControl) { 89 | presenter.selectEvent(at: segmentedControl.selectedSegmentIndex) 90 | } 91 | 92 | lazy var segmentedControlTopConstraint: NSLayoutConstraint = { 93 | if #available(iOS 11.0, *) { 94 | return segmentedControl.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8) 95 | } else { 96 | return segmentedControl.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor, constant: 8) 97 | } 98 | }() 99 | 100 | private func makeTimetableConfiguration(with startDate: Date?, and endDate: Date?) -> TimetableViewConfiguration { 101 | return TimetableViewConfiguration( 102 | itemWidth: self.itemWidth, 103 | heightOfHour: self.heightOfHour, 104 | itemEdgeInsets: UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), 105 | startDate: startDate, 106 | endDate: endDate, 107 | dataSource: self) 108 | } 109 | 110 | lazy var timetableView: TimetableView = { 111 | let timetableView = TimetableView(configuration: makeTimetableConfiguration(with: nil, and: nil)) 112 | timetableView.backgroundColor = .white 113 | timetableView.register(TimetableCell.self, forCellWithReuseIdentifier: TimetableCell.reuseIdentifier) 114 | timetableView.delegate = self 115 | if #available(iOS 11.0, *) { 116 | timetableView.contentInsetAdjustmentBehavior = .never 117 | } 118 | return timetableView 119 | }() 120 | 121 | lazy var tracksScrollView: TracksScrollView = { 122 | let scrollView = TracksScrollView(trackNames: [], itemWidth: self.itemWidth) 123 | scrollView.backgroundColor = .white 124 | scrollView.delegate = self 125 | if #available(iOS 11.0, *) { 126 | scrollView.contentInsetAdjustmentBehavior = .never 127 | } 128 | return scrollView 129 | }() 130 | 131 | lazy var timesScrollView: TimesScrollView = { 132 | let scrollView = TimesScrollView(startDate: nil, endDate: nil, heightOfHour: self.heightOfHour) 133 | scrollView.backgroundColor = .white 134 | scrollView.delegate = self 135 | if #available(iOS 11.0, *) { 136 | scrollView.contentInsetAdjustmentBehavior = .never 137 | } 138 | return scrollView 139 | }() 140 | 141 | // MARK: - TimetableViewProtocol 142 | 143 | func showEvents() { 144 | segmentedControl.removeAllSegments() 145 | 146 | let width: CGFloat = segmentedControl.frame.size.width 147 | let itemWidth = width / CGFloat(presenter.numberOfEvents()) 148 | (0.. Int { 173 | guard let event = presenter.currentEvent else { return 0 } 174 | 175 | return event.tracks.count 176 | } 177 | 178 | func timetableView(_ timetableView: TimetableView, numberOfItemsIn section: Int) -> Int { 179 | guard let event = presenter.currentEvent else { return 0 } 180 | 181 | guard section < event.tracks.count else { return 0 } 182 | let track = event.tracks[section] 183 | return track.sessions.count 184 | } 185 | 186 | func timetableView(_ timetableView: TimetableView, itemAt indexPath: IndexPath) -> TimetableViewItem? { 187 | guard let event = presenter.currentEvent else { return nil } 188 | return event.tracks[indexPath.section].sessions[indexPath.item] 189 | } 190 | 191 | func timetableView(_ timetableView: TimetableView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 192 | let cell = TimetableCell.dequeue(for: timetableView, at: indexPath) 193 | 194 | if let event = presenter.currentEvent { 195 | cell.item = event.tracks[indexPath.section].sessions[indexPath.row] 196 | } 197 | 198 | return cell 199 | } 200 | 201 | // MARK: - UIScrollViewDelegate 202 | 203 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 204 | if scrollView.isEqual(timesScrollView) { 205 | var contentOffset = timetableView.contentOffset 206 | contentOffset.y = scrollView.contentOffset.y 207 | timetableView.contentOffset = contentOffset 208 | } else if scrollView.isEqual(tracksScrollView) { 209 | var contentOffset = timetableView.contentOffset 210 | contentOffset.x = scrollView.contentOffset.x 211 | timetableView.contentOffset = contentOffset 212 | } else if scrollView.isEqual(timetableView) { 213 | times: do { 214 | var contentOffset = timesScrollView.contentOffset 215 | contentOffset.y = scrollView.contentOffset.y 216 | timesScrollView.contentOffset = contentOffset 217 | } 218 | 219 | tracks: do { 220 | var contentOffset = tracksScrollView.contentOffset 221 | contentOffset.x = scrollView.contentOffset.x 222 | tracksScrollView.contentOffset = contentOffset 223 | } 224 | } 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timetable/View/TimesScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimesScrollView.swift 3 | // TimetableViewSampler 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/21. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TimesScrollView: UIScrollView { 12 | var startDate: Date? 13 | var endDate: Date? 14 | var heightOfHour: CGFloat 15 | init(startDate: Date?, endDate: Date?, heightOfHour: CGFloat) { 16 | self.startDate = startDate 17 | self.endDate = endDate 18 | self.heightOfHour = heightOfHour 19 | 20 | super.init(frame: .zero) 21 | 22 | setUp() 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | private lazy var setUp: () -> () = { 30 | showsVerticalScrollIndicator = false 31 | showsHorizontalScrollIndicator = false 32 | 33 | showDates() 34 | 35 | return {} 36 | }() 37 | 38 | func update(startDate: Date, endDate: Date) { 39 | self.startDate = startDate 40 | self.endDate = endDate 41 | 42 | labels.forEach { $0.removeFromSuperview() } 43 | showDates() 44 | } 45 | 46 | private func showDates() { 47 | guard let startDate = startDate, let endDate = endDate else { return } 48 | 49 | var currentDate: Date = startDate 50 | repeat { 51 | let diff = currentDate.timeIntervalSince1970 - startDate.timeIntervalSince1970 52 | 53 | let y = height(for: diff) 54 | let label = makeLabel(with: currentDate.toString(with: "HH:mm")) 55 | label.frame = CGRect(x: 0, y: y, width: self.frame.size.width, height: 14) 56 | addSubview(label) 57 | labels.append(label) 58 | 59 | let next = nextHour(of: currentDate) 60 | currentDate = next 61 | } while currentDate < endDate 62 | } 63 | 64 | override func layoutSubviews() { 65 | super.layoutSubviews() 66 | 67 | labels.forEach { label in 68 | var frame = label.frame 69 | frame.size.width = self.frame.size.width 70 | label.frame = frame 71 | } 72 | 73 | guard let startDate = startDate, let endDate = endDate else { return } 74 | 75 | self.contentSize = CGSize(width: self.frame.size.width, height: height(for: endDate.timeIntervalSince1970 - startDate.timeIntervalSince1970)) 76 | } 77 | 78 | private func nextHour(of date: Date) -> Date { 79 | let currentComponents = Calendar.current.dateComponents([.year, .month, .day, .hour], from: date) 80 | let nextComponents = DateComponents(calendar: .current, timeZone: .current, year: currentComponents.year!, month: currentComponents.month!, day: currentComponents.day!, hour: currentComponents.hour! + 1) 81 | return Calendar.current.date(from: nextComponents)! 82 | } 83 | 84 | private func height(for timeInterval: TimeInterval) -> CGFloat { 85 | let hour = timeInterval / 3600 86 | return heightOfHour * CGFloat(hour) 87 | } 88 | 89 | private var labels: [UILabel] = [] 90 | 91 | private func makeLabel(with text: String) -> UILabel { 92 | let label = UILabel() 93 | label.font = UIFont.systemFont(ofSize: 14) 94 | label.textColor = UIColor.rc.mainText 95 | label.text = text 96 | label.numberOfLines = 0 97 | label.lineBreakMode = .byWordWrapping 98 | label.textAlignment = .right 99 | return label 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timetable/View/TimetableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimetableCell.swift 3 | // TimetableViewSampler 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/21. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TimetableCell: UICollectionViewCell, ReusableCollectionViewCell { 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | setUp() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | 22 | setUp() 23 | } 24 | 25 | private lazy var setUp: () -> () = { 26 | contentView.backgroundColor = UIColor.rc.background 27 | 28 | contentView.addSubview(titleLabel, constraints: [ 29 | titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), 30 | titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), 31 | contentView.trailingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 8), 32 | contentView.bottomAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: 8) 33 | ]) 34 | 35 | return {} 36 | }() 37 | 38 | lazy var titleLabel: UILabel = { 39 | let label = UILabel() 40 | label.numberOfLines = 0 41 | label.lineBreakMode = .byWordWrapping 42 | label.textAlignment = .left 43 | label.setContentHuggingPriority(.defaultLow, for: .horizontal) 44 | label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 45 | return label 46 | }() 47 | 48 | var item: Event.Track.Session? { 49 | didSet { 50 | guard let item = item else { 51 | titleLabel.text = nil 52 | return 53 | } 54 | 55 | let attributedString = NSMutableAttributedString(string: item.title, attributes: [ 56 | .font: UIFont.systemFont(ofSize: 16), 57 | .foregroundColor: UIColor.rc.mainText 58 | ]) 59 | if let author = item.author { 60 | attributedString.append(NSAttributedString(string: String(format: " %@", author), attributes: [ 61 | .font: UIFont.systemFont(ofSize: 16), 62 | .foregroundColor: UIColor.rc.link 63 | ])) 64 | } 65 | attributedString.append(NSAttributedString(string: String(format: " %@ - %@", item.startAt.toString(with: "HH:mm"), item.endAt.toString(with: "HH:mm")), attributes: [ 66 | .font: UIFont.systemFont(ofSize: 14), 67 | .foregroundColor: UIColor.rc.subText 68 | ])) 69 | titleLabel.attributedText = attributedString 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /iOSDCRC/ViewControllers/Timetable/View/TracksScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TracksScrollView.swift 3 | // TimetableViewSampler 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/21. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TracksScrollView: UIScrollView { 12 | var itemWidth: CGFloat { 13 | didSet { 14 | setNeedsLayout() 15 | } 16 | } 17 | 18 | var trackNames: [String] { 19 | didSet { 20 | labels.forEach { $0.removeFromSuperview() } 21 | showTrackNames() 22 | } 23 | } 24 | init(trackNames: [String], itemWidth: CGFloat) { 25 | self.trackNames = trackNames 26 | self.itemWidth = itemWidth 27 | 28 | super.init(frame: .zero) 29 | 30 | setUp() 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | private lazy var setUp: () -> () = { 38 | showsVerticalScrollIndicator = false 39 | showsHorizontalScrollIndicator = false 40 | 41 | showTrackNames() 42 | 43 | return {} 44 | }() 45 | 46 | private func showTrackNames() { 47 | trackNames.enumerated().forEach({ item in 48 | let label = makeLabel(with: item.element) 49 | label.frame = CGRect(x: CGFloat(item.offset) * itemWidth, y: 0, width: itemWidth, height: self.frame.size.height) 50 | addSubview(label) 51 | labels.append(label) 52 | }) 53 | } 54 | 55 | override func layoutSubviews() { 56 | super.layoutSubviews() 57 | 58 | labels.forEach { (label) in 59 | var frame = label.frame 60 | frame.size.height = self.frame.size.height 61 | label.frame = frame 62 | } 63 | 64 | self.contentSize = CGSize(width: itemWidth * CGFloat(trackNames.count), height: self.frame.size.height) 65 | } 66 | 67 | var labels: [UILabel] = [] 68 | 69 | private func makeLabel(with text: String) -> UILabel { 70 | let label = UILabel() 71 | label.textAlignment = .center 72 | label.font = UIFont.systemFont(ofSize: 16) 73 | label.textColor = UIColor.rc.mainText 74 | label.text = text 75 | return label 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /iOSDCRC/Views/Atomic/UIColor+rc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+rc.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/26. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | /// 色情報を定義 14 | struct RC { 15 | let mainText: UIColor = #colorLiteral(red: 0.2, green: 0.2, blue: 0.2, alpha: 1) 16 | let subText: UIColor = #colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) 17 | let link: UIColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1) 18 | let background: UIColor = #colorLiteral(red: 0.9333333333, green: 0.9333333333, blue: 0.9333333333, alpha: 1) 19 | } 20 | 21 | static let rc: RC = RC() 22 | } 23 | -------------------------------------------------------------------------------- /iOSDCRC/Views/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // iOSDCRC 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/24. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoadingView: UIView { 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | 15 | setUp() 16 | } 17 | 18 | required init?(coder aDecoder: NSCoder) { 19 | super.init(coder: aDecoder) 20 | 21 | setUp() 22 | } 23 | 24 | private lazy var setUp: () -> () = { 25 | isHidden = true 26 | backgroundColor = .white 27 | 28 | addSubview(indicatorView, constraints: [ 29 | indicatorView.centerXAnchor.constraint(equalTo: centerXAnchor), 30 | indicatorView.centerYAnchor.constraint(equalTo: centerYAnchor) 31 | ]) 32 | 33 | return {} 34 | }() 35 | 36 | lazy var indicatorView: UIActivityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray) 37 | 38 | func startAnimating() { 39 | isHidden = false 40 | indicatorView.startAnimating() 41 | } 42 | 43 | func stopAnimating() { 44 | isHidden = true 45 | indicatorView.stopAnimating() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /iOSDCRCTests/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 | -------------------------------------------------------------------------------- /iOSDCRCTests/iOSDCRCTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iOSDCRCTests.swift 3 | // iOSDCRCTests 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import iOSDCRC 11 | 12 | class iOSDCRCTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /iOSDCRCUITests/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 | -------------------------------------------------------------------------------- /iOSDCRCUITests/iOSDCRCUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iOSDCRCUITests.swift 3 | // iOSDCRCUITests 4 | // 5 | // Created by Kazuya Ueoka on 2018/07/22. 6 | // Copyright © 2018 Kazuya Ueoka. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class iOSDCRCUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | --------------------------------------------------------------------------------