├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── Podfile ├── Podfile.lock ├── QiitaPocket.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── pivot.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings └── xcuserdata │ ├── pivot.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ ├── QiitaPocket.xcscheme │ │ └── xcschememanagement.plist │ └── sakamoto.xcuserdatad │ └── xcschemes │ ├── qiitareader.xcscheme │ └── xcschememanagement.plist ├── QiitaPocket.xcworkspace └── contents.xcworkspacedata ├── QiitaPocket ├── API │ ├── APIClient.swift │ ├── ConnectionError.swift │ ├── Dependencies.swift │ ├── JSONDecodable.swift │ ├── QiitaAPI.swift │ ├── QiitaAPIError.swift │ └── QiitaRequest.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-1024.png │ │ ├── Icon-29@2x.png │ │ ├── Icon-29@3x.png │ │ ├── Icon-40.png │ │ ├── Icon-40@2x.png │ │ ├── Icon-40@3x.png │ │ ├── Icon-60.png │ │ ├── Icon-60@2x.png │ │ └── Icon-60@3x.png │ ├── ArticleListViewController.swift │ ├── Contents.json │ ├── icon │ │ ├── Contents.json │ │ ├── ic-check.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-check-1.png │ │ │ └── ic-check.png │ │ ├── ic-check_disabled.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-check_disabled-1.png │ │ │ └── ic-check_disabled-2.png │ │ ├── ic-check_white.imageset │ │ │ ├── Contents.json │ │ │ └── ic-check_white.png │ │ ├── ic-delete.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-delete-1.png │ │ │ └── ic-delete.png │ │ ├── ic-delete_history.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-delete_history-1.png │ │ │ └── ic-delete_history.png │ │ ├── ic-delete_history_on.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-delete_history_on-1.png │ │ │ └── ic-delete_history_on.png │ │ ├── ic-delete_on.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-delete_on-1.png │ │ │ └── ic-delete_on.png │ │ ├── ic-like.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-like-1.png │ │ │ └── ic-like.png │ │ ├── ic-rankBadge.imageset │ │ │ ├── Contents.json │ │ │ ├── icon_100490_64-1.png │ │ │ └── icon_100490_64.png │ │ ├── ic-read-later.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-read-later-1.png │ │ │ └── ic-read-later.png │ │ ├── ic-read-later_disabled.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-read-later_disabled-1.png │ │ │ └── ic-read-later_disabled-2.png │ │ ├── ic-read-later_white.imageset │ │ │ ├── Contents.json │ │ │ └── ic-read-later_white.png │ │ ├── ic-search.imageset │ │ │ ├── Contents.json │ │ │ └── ic-search.png │ │ ├── ic-setting.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-setting@2x.png │ │ │ └── ic-setting@3x.png │ │ ├── ic-tab-read-later.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-tab-read-later@2x.png │ │ │ └── ic-tab-read-later@3x.png │ │ ├── ic-tab-read-later_selected.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-tab-read-later_selected@2x.png │ │ │ └── ic-tab-read-later_selected@3x.png │ │ ├── ic-tab-search.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-tab-search@2x.png │ │ │ └── ic-tab-search@3x.png │ │ └── ic-tab-search_selected.imageset │ │ │ ├── Contents.json │ │ │ ├── ic-tab-search_selected@2x.png │ │ │ └── ic-tab-search_selected@3x.png │ └── logo.imageset │ │ ├── Contents.json │ │ ├── logo.png │ │ ├── logo@2x.png │ │ └── logo@3x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Extension │ ├── ArrayExtension.swift │ ├── UIColorExtension.swift │ ├── UIScrollView+Rx.swift │ └── UIViewExtension.swift ├── GoogleService-Info.plist ├── Info.plist ├── Model │ ├── Article.swift │ ├── ArticleManager.swift │ ├── Articles.swift │ ├── FileName.swift │ ├── License.swift │ ├── LicenseDetail.swift │ ├── SearchHistory.swift │ ├── SearchPeriod.swift │ └── SearchType.swift ├── Util │ ├── UserSettings.swift │ └── Util.swift ├── View │ ├── ArchiveTableViewCell.swift │ ├── ArchiveTableViewCell.xib │ ├── ArticleCellType.swift │ ├── ArticleTableViewCell.swift │ ├── ArticleTableViewCell.xib │ ├── ArticleView.swift │ ├── ArticleView.xib │ ├── ReadLaterTableViewCell.swift │ ├── ReadLaterTableViewCell.xib │ ├── SearchHistoryTableViewCell.swift │ └── SwipeCellType.swift ├── ViewController │ ├── ArchiveViewController.swift │ ├── ArticleListNavigationController.swift │ ├── ArticleListViewController.swift │ ├── ArticleTableViewDataSource.swift │ ├── BannerViewType.swift │ ├── LicenseDetailViewController.swift │ ├── LicensesViewController.swift │ ├── MainTabBarController.swift │ ├── OtherNavigationController.swift │ ├── OtherTableViewController.swift │ ├── OtherViewController.swift │ ├── ReadLaterTabViewController.swift │ ├── ReadLaterViewController.swift │ └── SearchArticleViewController.swift ├── ViewModel │ └── ArticleListViewModel.swift └── com.mono0926.LicensePlist │ ├── Alamofire.plist │ ├── Firebase.plist │ ├── FirebaseAnalytics.plist │ ├── FirebaseCore.plist │ ├── FirebaseCrash.plist │ ├── FirebaseInstanceID.plist │ ├── Google-Mobile-Ads-SDK.plist │ ├── GoogleToolboxForMac.plist │ ├── Protobuf.plist │ ├── RxDataSources.plist │ ├── RxSwift.plist │ ├── SDWebImage.plist │ ├── SwiftyJSON.plist │ ├── XLPagerTabStrip.plist │ ├── com.mono0926.LicensePlist.plist │ └── realm-cocoa.plist └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | Carthage/Checkouts 52 | Carthage/Build 53 | 54 | # fastlane 55 | # 56 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 57 | # screenshots whenever they are needed. 58 | # For more information about the recommended setup visit: 59 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 60 | 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | 66 | # keys 67 | Keys.plist 68 | 69 | # Firebase 70 | qiita-pocket-firebase-crashreporting-qysod-da92c66dbb.json 71 | qiita-pocket-firebase-crashreporting-qysod-61f4f8b587.json 72 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "Alamofire/Alamofire" 2 | github "SwiftyJSON/SwiftyJSON" ~> 4.0 3 | github "rs/SDWebImage" 4 | github "ReactiveX/RxSwift" ~> 4.0 5 | github "realm/realm-cocoa" ~> 2.0 6 | github "xmartlabs/XLPagerTabStrip" "swift4" 7 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Alamofire/Alamofire" "4.5.1" 2 | github "ReactiveX/RxSwift" "4.0.0-alpha.1" 3 | github "SwiftyJSON/SwiftyJSON" "4.0.0-alpha.1" 4 | github "realm/realm-cocoa" "v2.10.0" 5 | github "rs/SDWebImage" "4.1.0" 6 | github "xmartlabs/XLPagerTabStrip" "df0840af3c90d10beb0d7b715a8d5c4d78c62fe7" 7 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # platform :ios, ’10.0’ 2 | 3 | target 'QiitaPocket' do 4 | use_frameworks! 5 | 6 | # Pods for QiitaPocket 7 | pod 'Firebase/Core' 8 | pod 'Firebase/Crash' 9 | pod 'Firebase/AdMob' 10 | end 11 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Firebase/AdMob (4.3.0): 3 | - Firebase/Core 4 | - Google-Mobile-Ads-SDK (= 7.24.1) 5 | - Firebase/Core (4.3.0): 6 | - FirebaseAnalytics (= 4.0.4) 7 | - FirebaseCore (= 4.0.8) 8 | - Firebase/Crash (4.3.0): 9 | - Firebase/Core 10 | - FirebaseCrash (= 2.0.2) 11 | - FirebaseAnalytics (4.0.4): 12 | - FirebaseCore (~> 4.0) 13 | - FirebaseInstanceID (~> 2.0) 14 | - GoogleToolboxForMac/NSData+zlib (~> 2.1) 15 | - nanopb (~> 0.3) 16 | - FirebaseCore (4.0.8): 17 | - GoogleToolboxForMac/NSData+zlib (~> 2.1) 18 | - nanopb (~> 0.3) 19 | - FirebaseCrash (2.0.2): 20 | - FirebaseAnalytics (~> 4.0) 21 | - FirebaseInstanceID (~> 2.0) 22 | - GoogleToolboxForMac/Logger (~> 2.1) 23 | - GoogleToolboxForMac/NSData+zlib (~> 2.1) 24 | - Protobuf (~> 3.1) 25 | - FirebaseInstanceID (2.0.4) 26 | - Google-Mobile-Ads-SDK (7.24.1) 27 | - GoogleToolboxForMac/Defines (2.1.1) 28 | - GoogleToolboxForMac/Logger (2.1.1): 29 | - GoogleToolboxForMac/Defines (= 2.1.1) 30 | - GoogleToolboxForMac/NSData+zlib (2.1.1): 31 | - GoogleToolboxForMac/Defines (= 2.1.1) 32 | - nanopb (0.3.8): 33 | - nanopb/decode (= 0.3.8) 34 | - nanopb/encode (= 0.3.8) 35 | - nanopb/decode (0.3.8) 36 | - nanopb/encode (0.3.8) 37 | - Protobuf (3.4.0) 38 | 39 | DEPENDENCIES: 40 | - Firebase/AdMob 41 | - Firebase/Core 42 | - Firebase/Crash 43 | 44 | SPEC CHECKSUMS: 45 | Firebase: 83283761a1ef6dc9846e03d08059f51421afbd65 46 | FirebaseAnalytics: 722b53c7b32bfc7806b06e0093a2f5180d4f2c5a 47 | FirebaseCore: 69b1a5ac5f857ba6d5fd9d5fe794f4786dd5e579 48 | FirebaseCrash: cded0fc566c03651aea606a101bc156085f333ca 49 | FirebaseInstanceID: 70c2b877e9338971b2429ea5a4293df6961aa44e 50 | Google-Mobile-Ads-SDK: ed8004a7265b424568dc84f3d2bbe3ea3fff958f 51 | GoogleToolboxForMac: 8e329f1b599f2512c6b10676d45736bcc2cbbeb0 52 | nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 53 | Protobuf: 03eef2ee0b674770735cf79d9c4d3659cf6908e8 54 | 55 | PODFILE CHECKSUM: 3fb90e97fd2498d683abaaa0e5ffcb2ff866b5bc 56 | 57 | COCOAPODS: 1.1.1 58 | -------------------------------------------------------------------------------- /QiitaPocket.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /QiitaPocket.xcodeproj/project.xcworkspace/xcuserdata/pivot.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket.xcodeproj/project.xcworkspace/xcuserdata/pivot.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /QiitaPocket.xcodeproj/project.xcworkspace/xcuserdata/pivot.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /QiitaPocket.xcodeproj/xcuserdata/pivot.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /QiitaPocket.xcodeproj/xcuserdata/pivot.xcuserdatad/xcschemes/QiitaPocket.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /QiitaPocket.xcodeproj/xcuserdata/pivot.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | QiitaPocket.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 52471A8C1CD9AF2E006865C6 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /QiitaPocket.xcodeproj/xcuserdata/sakamoto.xcuserdatad/xcschemes/qiitareader.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /QiitaPocket.xcodeproj/xcuserdata/sakamoto.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | qiitareader.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 52471A8C1CD9AF2E006865C6 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /QiitaPocket.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /QiitaPocket/API/APIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // qiitareader 4 | // 5 | // Created by hirothings on 2016/05/04. 6 | // Copyright © 2016年 hiroshings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import RxSwift 12 | 13 | class APIClient { 14 | 15 | 16 | /// Observable化したAPIレスポンスを返す 17 | func call(request: Request) -> Observable { 18 | 19 | return Observable.create { [weak self] observer -> Disposable in 20 | guard let `self` = self else { return Disposables.create {} } 21 | 22 | let url = self.buildPath(baseURL: request.baseURL, path: request.path) 23 | 24 | let request = Alamofire.request(url, method: request.method, parameters: request.parameters, headers: nil) 25 | .responseJSON { response in 26 | 27 | var nextPage: Int? = nil 28 | let nextPageStr: String? = self.parseNextPage(header: response.response?.allHeaderFields) 29 | if let nextPageStr = nextPageStr { 30 | nextPage = Int(nextPageStr) 31 | } 32 | 33 | switch response.result { 34 | case .success(let value): 35 | // QiitaAPIのエラー処理 36 | if let qiitaAPIError = QiitaAPIError(json: value) { 37 | observer.onError(qiitaAPIError) 38 | } 39 | if let json = value as? [Any] { 40 | let responseObject = Request.ResponseObject(json: json, nextPage: nextPage) 41 | observer.on(.next(responseObject)) 42 | } 43 | observer.on(.completed) 44 | case .failure(let error): 45 | let connectingError = ConnectionError(errorCode: error._code) 46 | observer.on(.error(connectingError)) 47 | } 48 | } 49 | 50 | request.resume() 51 | 52 | return Disposables.create { 53 | request.cancel() 54 | } 55 | } 56 | } 57 | 58 | /// "/"が先頭にある場合、それ以降の文字列を取得 59 | private func buildPath(baseURL: String, path: String) -> URL { 60 | let trimmedPath = path.hasPrefix("/") ? path.substring(to: path.characters.index(after: path.startIndex)) : path 61 | return URL(string: baseURL + "/" + trimmedPath)! 62 | } 63 | 64 | /// 次のページ番号をLinkヘッダーからParseする 65 | private func parseNextPage(header: [AnyHashable: Any]?) -> String? { 66 | guard let serializedLinks = header?["Link"] as? String else { return nil } 67 | do { 68 | let regex = try NSRegularExpression(pattern: "(?<=page=)(.+?)(?=.*rel=\"next\")", options: .allowCommentsAndWhitespace) 69 | guard let match = regex.firstMatch(in: serializedLinks, 70 | options: NSRegularExpression.MatchingOptions(), 71 | range: NSRange(location: 0, length: serializedLinks.characters.count)) else { return nil } 72 | 73 | let matcheStrings = (1 ..< match.numberOfRanges).map { rangeIndex -> String in 74 | let range = match.range(at: rangeIndex) // マッチングした位置 75 | // CharacterView型のindexに変換 76 | let startIndex: String.CharacterView.Index = serializedLinks.characters.index(serializedLinks.startIndex, offsetBy: range.location) 77 | let endIndex: String.CharacterView.Index = serializedLinks.characters.index(serializedLinks.startIndex, offsetBy: range.location + range.length) 78 | let stringRange = startIndex ..< endIndex 79 | return serializedLinks.substring(with: stringRange) // マッチした文字列を抜き出す 80 | } 81 | return matcheStrings.first 82 | } 83 | catch { 84 | return nil 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /QiitaPocket/API/ConnectionError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionError.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/20. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ConnectionError: Error { 12 | var message: String 13 | 14 | init(errorCode: Int) { 15 | 16 | switch errorCode { 17 | case -1001: 18 | message = "通信がタイムアウトしました。電波環境の良い場所で再度お試しください" 19 | case -1009: 20 | message = "ネットワークに接続されていません" 21 | default: 22 | message = "通信エラーが発生しました" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /QiitaPocket/API/Dependencies.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dependencies.swift 3 | // qiitareader 4 | // 5 | // Created by hirothings on 2016/07/18. 6 | // Copyright © 2016年 hiroshings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | class Dependencies { 13 | 14 | // MARK: - Properties 15 | 16 | let mainScheduler: SerialDispatchQueueScheduler = MainScheduler.instance 17 | let backgroundScheduler: ImmediateSchedulerType = { 18 | let operationQueue = OperationQueue() 19 | operationQueue.maxConcurrentOperationCount = 2 20 | operationQueue.qualityOfService = QualityOfService.userInitiated 21 | 22 | return OperationQueueScheduler(operationQueue: operationQueue) 23 | }() 24 | 25 | static let sharedInstance = Dependencies() 26 | 27 | // MARK: - Initializers 28 | 29 | fileprivate init() {} 30 | } 31 | -------------------------------------------------------------------------------- /QiitaPocket/API/JSONDecodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDecodable.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/26. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol JSONDecodable { 12 | init(json: [Any], nextPage: Int?) // TODO: エラー処理 13 | } 14 | -------------------------------------------------------------------------------- /QiitaPocket/API/QiitaAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QiitaAPI.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/26. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class QiitaAPI { 12 | 13 | /// 投稿記事のリクエスト 14 | struct SearchArticles: QiitaRequest { 15 | typealias ResponseObject = Articles 16 | 17 | let tag: String 18 | let page: Int 19 | 20 | // Qiita API v2 21 | var baseURL: String { 22 | return "https://qiita.com/api/v2" 23 | } 24 | 25 | var path: String { 26 | return "items" 27 | } 28 | 29 | var parameters: [String: Any]? { 30 | if tag.isEmpty { 31 | return ["page": page] 32 | } 33 | else { 34 | return ["page": page, "query": tag] 35 | } 36 | } 37 | } 38 | 39 | /// ランキング記事のリクエスト 40 | struct SearchRankedPost: QiitaRequest { 41 | typealias ResponseObject = Articles 42 | 43 | let tag: String 44 | let period: SearchPeriod 45 | 46 | // Qiita Pocket用自作API 47 | var baseURL: String { 48 | return "https://qiita-pocket-api.herokuapp.com" 49 | } 50 | 51 | var path: String { 52 | return "articles" 53 | } 54 | 55 | var parameters: [String: Any]? { 56 | if tag.isEmpty { 57 | return ["period": period.rawValue] 58 | } 59 | else { 60 | return [ 61 | "period": period.rawValue, 62 | "tag": tag 63 | ] 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /QiitaPocket/API/QiitaAPIError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QiitaAPIError.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/20. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct QiitaAPIError: Error { 12 | 13 | var message: String = "" 14 | 15 | 16 | init?(json: Any) { 17 | guard let dict = json as? [String: Any] else { 18 | return nil 19 | } 20 | 21 | guard let originMessage = dict["error"] as? String else { 22 | return nil 23 | } 24 | 25 | switch originMessage { 26 | case "Rate limit exceeded.": 27 | message = "Qiita APIのRateLimitに達しました。しばらく経ってからご利用ください" 28 | default: 29 | message = "APIエラーが発生しました" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /QiitaPocket/API/QiitaRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QiitaRequest.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/26. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | protocol QiitaRequest { 13 | associatedtype ResponseObject: JSONDecodable 14 | 15 | var baseURL: String { get } 16 | var path: String { get } 17 | var method: HTTPMethod { get } 18 | var parameters: [String: Any]? { get } 19 | } 20 | 21 | extension QiitaRequest { 22 | var method: HTTPMethod { 23 | return Alamofire.HTTPMethod.get 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /QiitaPocket/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // qiitareader 4 | // 5 | // Created by hirothings on 2016/05/04. 6 | // Copyright © 2016年 hiroshings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Firebase 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 19 | FirebaseApp.configure() 20 | GADMobileAds.configure(withApplicationID: "ca-app-pub-8842953390661934~5974140004") 21 | return true 22 | } 23 | 24 | func applicationWillResignActive(_ application: UIApplication) { 25 | // 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. 26 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 27 | } 28 | 29 | func applicationDidEnterBackground(_ application: UIApplication) { 30 | // 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. 31 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 32 | } 33 | 34 | func applicationWillEnterForeground(_ application: UIApplication) { 35 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 36 | } 37 | 38 | func applicationDidBecomeActive(_ application: UIApplication) { 39 | // 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. 40 | } 41 | 42 | func applicationWillTerminate(_ application: UIApplication) { 43 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 44 | } 45 | 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /QiitaPocket/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-29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "1024x1024", 53 | "idiom" : "ios-marketing", 54 | "filename" : "Icon-1024.png", 55 | "scale" : "1x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-60.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/ArticleListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleListViewController.swift 3 | // 4 | // 5 | // Created by 坂本 浩 on 2016/05/04. 6 | // 7 | // 8 | 9 | import UIKit 10 | import WebImage 11 | import RxSwift 12 | 13 | 14 | class ArticleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate { 15 | 16 | @IBOutlet weak var table: UITableView! 17 | 18 | var articles: [Article] = [] 19 | var postUrl: URL? 20 | var refreshControll: UIRefreshControl! 21 | 22 | private let bag = DisposeBag() 23 | 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | table.rowHeight = 40.0 29 | table.separatorInset = UIEdgeInsets.zero 30 | title = "新着記事" 31 | 32 | setupSearchBar() 33 | 34 | refreshControll = UIRefreshControl() 35 | refreshControll.attributedTitle = NSAttributedString(string: "下に引っ張って更新") 36 | pullToRefresh() 37 | 38 | table.addSubview(refreshControll) 39 | } 40 | 41 | /// tableViewの行数を指定 42 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 43 | return articles.count 44 | } 45 | 46 | /// tableViewのcellを生成 47 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 48 | 49 | let cell = table.dequeueReusableCell(withIdentifier: "ArticleTableViewCell", for: indexPath) as! ArticleTableViewCell 50 | let article = articles[indexPath.row] 51 | 52 | print("cell生成") 53 | // cell.setCell(article: article) 54 | // label.lineBreakMode = .byTruncatingTail 55 | 56 | return cell 57 | } 58 | 59 | /// tableViewタップ時の処理 60 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 61 | 62 | let article = articles[indexPath.row] 63 | 64 | postUrl = URL(string: article.url) 65 | performSegue(withIdentifier: "toWebView", sender: nil) 66 | } 67 | 68 | /// 各segueの設定 69 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 70 | 71 | switch segue.identifier! { 72 | case "toWebView": 73 | let webView: WebViewController = segue.destination as! WebViewController 74 | webView.url = postUrl 75 | default: 76 | break 77 | } 78 | } 79 | 80 | func pullToRefresh() { 81 | 82 | self.refreshControll.rx.controlEvent(.valueChanged) 83 | .asObservable() 84 | .startWith(()) 85 | .flatMap { 86 | return Article.fetch() 87 | } 88 | .subscribe(onNext: { [unowned self] result in 89 | 90 | print("fetch done") 91 | 92 | self.articles = result 93 | self.table.delegate = self 94 | self.table.dataSource = self 95 | self.table.reloadData() 96 | 97 | self.refreshControll.endRefreshing() 98 | }) 99 | .addDisposableTo(bag) 100 | } 101 | 102 | 103 | private func setupSearchBar() { 104 | 105 | let navigationBarFrame: CGRect = self.navigationController!.navigationBar.bounds 106 | let searchBar: UISearchBar = UISearchBar(frame: navigationBarFrame) 107 | 108 | searchBar.delegate = self 109 | searchBar.placeholder = "タグを検索" 110 | searchBar.showsCancelButton = true 111 | searchBar.autocapitalizationType = .none 112 | searchBar.keyboardType = .default 113 | navigationItem.titleView = searchBar 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-check.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-check-1.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-check.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-check.imageset/ic-check-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-check.imageset/ic-check-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-check.imageset/ic-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-check.imageset/ic-check.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-check_disabled.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-check_disabled-1.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-check_disabled-2.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-check_disabled.imageset/ic-check_disabled-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-check_disabled.imageset/ic-check_disabled-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-check_disabled.imageset/ic-check_disabled-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-check_disabled.imageset/ic-check_disabled-2.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-check_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "ic-check_white.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-check_white.imageset/ic-check_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-check_white.imageset/ic-check_white.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-delete-1.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-delete.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete.imageset/ic-delete-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-delete.imageset/ic-delete-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete.imageset/ic-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-delete.imageset/ic-delete.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_history.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-delete_history.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-delete_history-1.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_history.imageset/ic-delete_history-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-delete_history.imageset/ic-delete_history-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_history.imageset/ic-delete_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-delete_history.imageset/ic-delete_history.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_history_on.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-delete_history_on.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-delete_history_on-1.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_history_on.imageset/ic-delete_history_on-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-delete_history_on.imageset/ic-delete_history_on-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_history_on.imageset/ic-delete_history_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-delete_history_on.imageset/ic-delete_history_on.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_on.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-delete_on.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-delete_on-1.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_on.imageset/ic-delete_on-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-delete_on.imageset/ic-delete_on-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-delete_on.imageset/ic-delete_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-delete_on.imageset/ic-delete_on.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-like.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-like.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-like-1.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-like.imageset/ic-like-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-like.imageset/ic-like-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-like.imageset/ic-like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-like.imageset/ic-like.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-rankBadge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon_100490_64-1.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon_100490_64.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-rankBadge.imageset/icon_100490_64-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-rankBadge.imageset/icon_100490_64-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-rankBadge.imageset/icon_100490_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-rankBadge.imageset/icon_100490_64.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-read-later.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-read-later.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-read-later-1.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-read-later.imageset/ic-read-later-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-read-later.imageset/ic-read-later-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-read-later.imageset/ic-read-later.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-read-later.imageset/ic-read-later.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-read-later_disabled.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-read-later_disabled-1.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-read-later_disabled-2.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-read-later_disabled.imageset/ic-read-later_disabled-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-read-later_disabled.imageset/ic-read-later_disabled-1.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-read-later_disabled.imageset/ic-read-later_disabled-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-read-later_disabled.imageset/ic-read-later_disabled-2.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-read-later_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "ic-read-later_white.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-read-later_white.imageset/ic-read-later_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-read-later_white.imageset/ic-read-later_white.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-search.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "ic-search.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-search.imageset/ic-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-search.imageset/ic-search.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-setting.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-setting@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-setting@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-setting.imageset/ic-setting@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-setting.imageset/ic-setting@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-setting.imageset/ic-setting@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-setting.imageset/ic-setting@3x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-read-later.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-tab-read-later@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-tab-read-later@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-read-later.imageset/ic-tab-read-later@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-tab-read-later.imageset/ic-tab-read-later@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-read-later.imageset/ic-tab-read-later@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-tab-read-later.imageset/ic-tab-read-later@3x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-read-later_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-tab-read-later_selected@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-tab-read-later_selected@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-read-later_selected.imageset/ic-tab-read-later_selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-tab-read-later_selected.imageset/ic-tab-read-later_selected@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-read-later_selected.imageset/ic-tab-read-later_selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-tab-read-later_selected.imageset/ic-tab-read-later_selected@3x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-search.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-tab-search@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-tab-search@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-search.imageset/ic-tab-search@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-tab-search.imageset/ic-tab-search@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-search.imageset/ic-tab-search@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-tab-search.imageset/ic-tab-search@3x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-search_selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic-tab-search_selected@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ic-tab-search_selected@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-search_selected.imageset/ic-tab-search_selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-tab-search_selected.imageset/ic-tab-search_selected@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/icon/ic-tab-search_selected.imageset/ic-tab-search_selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/icon/ic-tab-search_selected.imageset/ic-tab-search_selected@3x.png -------------------------------------------------------------------------------- /QiitaPocket/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 | } -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/logo.imageset/logo.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/logo.imageset/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/logo.imageset/logo@2x.png -------------------------------------------------------------------------------- /QiitaPocket/Assets.xcassets/logo.imageset/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirothings/qiita-pocket/88f76e32dda96faae4856b1f7bb02e659fe12438/QiitaPocket/Assets.xcassets/logo.imageset/logo@3x.png -------------------------------------------------------------------------------- /QiitaPocket/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /QiitaPocket/Extension/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtension.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/26. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | var isNotEmpty: Bool { 13 | return !self.isEmpty 14 | } 15 | } 16 | 17 | extension Collection where Indices.Iterator.Element == Index { 18 | 19 | subscript(safe index: Index) -> Iterator.Element? { 20 | return indices.contains(index) ? self[index] : nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /QiitaPocket/Extension/UIColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColorExtension.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/26. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIColor { 13 | static let theme = #colorLiteral(red: 0.2274509804, green: 0.6392156863, blue: 0.03921568627, alpha: 1) 14 | static let week = #colorLiteral(red: 0.7764705882, green: 0.7019607843, blue: 0.3882352941, alpha: 1) 15 | static let month = #colorLiteral(red: 0, green: 0.6923480034, blue: 0.7164273858, alpha: 1) 16 | static let bg = #colorLiteral(red: 0.9411764706, green: 0.9411764706, blue: 0.9411764706, alpha: 1) 17 | static let readLater = #colorLiteral(red: 0.9647058824, green: 0.6509803922, blue: 0.137254902, alpha: 1) 18 | static let disabled = #colorLiteral(red: 0.8470588235, green: 0.8470588235, blue: 0.8470588235, alpha: 1) 19 | static let rankGold = #colorLiteral(red: 0.7764705882, green: 0.7019607843, blue: 0.3882352941, alpha: 1) 20 | static let rankSilver = #colorLiteral(red: 0.6117647059, green: 0.662745098, blue: 0.7450980392, alpha: 1) 21 | static let rankBronse = #colorLiteral(red: 0.8156862745, green: 0.5490196078, blue: 0.3529411765, alpha: 1) 22 | } 23 | -------------------------------------------------------------------------------- /QiitaPocket/Extension/UIScrollView+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+Rx.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/05/03. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | extension Reactive where Base: UIScrollView { 14 | var reachedBottom: ControlEvent { 15 | let observable = contentOffset 16 | .flatMap { [weak base] contentOffset -> Observable in 17 | guard let scrollView = base else { 18 | return Observable.empty() 19 | } 20 | 21 | let visibleHeight = scrollView.frame.height - scrollView.contentInset.top - scrollView.contentInset.bottom 22 | let y = contentOffset.y + scrollView.contentInset.top 23 | let threshold = max(0.0, scrollView.contentSize.height - visibleHeight) 24 | 25 | return y > threshold ? Observable.just(()) : Observable.empty() 26 | } 27 | 28 | return ControlEvent(events: observable) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /QiitaPocket/Extension/UIViewExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtension.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/16. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | func fadeIn(duration: Double = 0.2) { 14 | UIView.animate(withDuration: duration, animations: { 15 | self.alpha = 1.0 16 | }) 17 | } 18 | 19 | func fadeOut(duration: Double = 0.2) { 20 | UIView.animate(withDuration: duration, animations: { 21 | self.alpha = 0.0 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /QiitaPocket/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AD_UNIT_ID_FOR_BANNER_TEST 6 | ca-app-pub-3940256099942544/2934735716 7 | AD_UNIT_ID_FOR_INTERSTITIAL_TEST 8 | ca-app-pub-3940256099942544/4411468910 9 | CLIENT_ID 10 | 428407217170-1bp33a84n99afgq72i3203re4k2knfo9.apps.googleusercontent.com 11 | REVERSED_CLIENT_ID 12 | com.googleusercontent.apps.428407217170-1bp33a84n99afgq72i3203re4k2knfo9 13 | API_KEY 14 | AIzaSyCpZbxw8dUJr45Wthu9s0mZGyfHMPrhX5o 15 | GCM_SENDER_ID 16 | 428407217170 17 | PLIST_VERSION 18 | 1 19 | BUNDLE_ID 20 | hirothings.QiitaPocket 21 | PROJECT_ID 22 | qiita-pocket 23 | STORAGE_BUCKET 24 | qiita-pocket.appspot.com 25 | IS_ADS_ENABLED 26 | 27 | IS_ANALYTICS_ENABLED 28 | 29 | IS_APPINVITE_ENABLED 30 | 31 | IS_GCM_ENABLED 32 | 33 | IS_SIGNIN_ENABLED 34 | 35 | GOOGLE_APP_ID 36 | 1:428407217170:ios:fb6e71cb93579d51 37 | DATABASE_URL 38 | https://qiita-pocket.firebaseio.com 39 | 40 | -------------------------------------------------------------------------------- /QiitaPocket/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Qiita Pocket 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 2.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 2 25 | LSApplicationCategoryType 26 | 27 | LSRequiresIPhoneOS 28 | 29 | NSAppTransportSecurity 30 | 31 | NSExceptionDomains 32 | 33 | qiita.com 34 | 35 | NSIncludesSubdomains 36 | 37 | NSTemporaryExceptionAllowsInsecureHTTPLoads 38 | 39 | NSTemporaryExceptionRequiresForwardSecrecy 40 | 41 | 42 | twimg.com 43 | 44 | NSIncludesSubdomains 45 | 46 | NSTemporaryExceptionAllowsInsecureHTTPLoads 47 | 48 | NSTemporaryExceptionRequiresForwardSecrecy 49 | 50 | 51 | 52 | 53 | UILaunchStoryboardName 54 | LaunchScreen 55 | UIMainStoryboardFile 56 | Main 57 | UIRequiredDeviceCapabilities 58 | 59 | armv7 60 | 61 | UISupportedInterfaceOrientations 62 | 63 | UIInterfaceOrientationPortrait 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /QiitaPocket/Model/Article.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Article.swift 3 | // qiitareader 4 | // 5 | // Created by hirothings on 2016/07/18. 6 | // Copyright © 2016年 hiroshings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import RxSwift 12 | import SwiftyJSON 13 | 14 | enum SaveState: String { 15 | case none 16 | case readLater 17 | case archive 18 | } 19 | 20 | final class Article: Object { 21 | 22 | @objc dynamic var updatedAt: Date = Date() 23 | @objc dynamic var publishedAt: String = "" 24 | @objc dynamic var id: String = "" 25 | @objc dynamic var title: String = "" 26 | @objc dynamic var author: String = "" 27 | @objc dynamic var profile_image_url: String = "" 28 | @objc dynamic var url: String = "" 29 | @objc dynamic var saveState: String = SaveState.none.rawValue 30 | @objc dynamic var hasSaved: Bool = false 31 | let tags: List = List() 32 | @objc dynamic var stockCount: Int = 0 33 | let rank = RealmOptional() 34 | 35 | var saveStateType: SaveState { 36 | get { 37 | return SaveState(rawValue: saveState)! 38 | } 39 | set { 40 | saveState = newValue.rawValue 41 | } 42 | } 43 | 44 | var formattedUpdatedAt: String { 45 | let formatter = DateFormatter() 46 | formatter.dateFormat = "yyyy.MM.dd HH:mm:ss" 47 | return formatter.string(from: updatedAt) 48 | } 49 | 50 | override class func primaryKey() -> String? { 51 | return "id" 52 | } 53 | } 54 | 55 | 56 | final class Tag: Object { 57 | @objc dynamic var name: String = "" 58 | 59 | override convenience init(value: Any) { 60 | self.init() 61 | self.name = value as! String 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /QiitaPocket/Model/ArticleManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleManager.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/01/15. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | final class ArticleManager { 13 | 14 | static let realm: Realm = try! Realm() 15 | 16 | /// 全件取得 17 | static func getAll() -> Results
{ 18 | return realm.objects(Article.self) 19 | } 20 | 21 | /// 後で読むを取得 22 | static func getReadLaters() -> Results
{ 23 | return realm.objects(Article.self).filter("saveState == '\(SaveState.readLater.rawValue)'").sorted(byKeyPath: "updatedAt", ascending: false) 24 | } 25 | 26 | /// アーカイブを取得 27 | static func getArchives() -> Results
{ 28 | return realm.objects(Article.self).filter("saveState == '\(SaveState.archive.rawValue)'").sorted(byKeyPath: "updatedAt", ascending: false) 29 | } 30 | 31 | /// 後で読むに追加 32 | static func add(readLater article: Article) { 33 | do { 34 | try realm.write { 35 | article.saveStateType = .readLater 36 | article.updatedAt = Date() 37 | realm.add(article, update: true) 38 | } 39 | } 40 | catch _ { 41 | // TODO: error処理 42 | } 43 | } 44 | 45 | /// アーカイブに追加 46 | static func add(archive article: Article) { 47 | do { 48 | try realm.write { 49 | article.saveStateType = .archive 50 | article.updatedAt = Date() 51 | realm.add(article, update: true) 52 | } 53 | } 54 | catch _ { 55 | // TODO: error処理 56 | } 57 | } 58 | 59 | /// 削除 60 | static func delete(article: Article) { 61 | do { 62 | try realm.write { 63 | realm.delete(article) 64 | } 65 | } 66 | catch _ { 67 | // TODO: error処理 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /QiitaPocket/Model/Articles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Articles.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/26. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import SwiftyJSON 12 | 13 | struct Articles: JSONDecodable { 14 | 15 | static let apiClient = APIClient() 16 | let items: [Article] 17 | let nextPage: Int? 18 | 19 | init(json: [Any], nextPage: Int?) { 20 | var rankCount: Int = 1 21 | 22 | items = json.map { res in 23 | let json = JSON(res) 24 | let article = Article() 25 | 26 | let createdAt = json["created_at"].stringValue 27 | article.publishedAt = Util.setDisplayDate(str: createdAt, format: "yyyy.MM.dd") 28 | article.id = json["id"].stringValue 29 | article.title = json["title"].stringValue 30 | article.author = json["user"]["id"].stringValue 31 | article.profile_image_url = json["user"]["profile_image_url"].stringValue 32 | article.url = json["url"].stringValue 33 | 34 | let tags = json["tags"].arrayValue 35 | .map { $0["name"].stringValue } 36 | .flatMap { Tag(value: $0) } 37 | 38 | for tag in tags { 39 | article.tags.append(tag) // imutableだからappendしかない? 40 | } 41 | if UserSettings.getSearchType() == .rank { 42 | article.rank.value = rankCount 43 | rankCount += 1 44 | } 45 | 46 | article.stockCount = json["likes_count"].intValue 47 | 48 | return article 49 | } 50 | 51 | self.nextPage = nextPage 52 | } 53 | 54 | static func fetch(with tag: String, page: Int) -> Observable { 55 | let request = QiitaAPI.SearchArticles(tag: tag, page: page) 56 | return self.apiClient.call(request: request) 57 | } 58 | 59 | static func fetchRankedPost(with tag: String, period: SearchPeriod) -> Observable { 60 | let request = QiitaAPI.SearchRankedPost(tag: tag, period: period) 61 | return self.apiClient.call(request: request) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /QiitaPocket/Model/FileName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileName.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/04/10. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum FileName: String { 12 | case keys = "Keys" 13 | case licenses = "com.mono0926.LicensePlist" 14 | } 15 | -------------------------------------------------------------------------------- /QiitaPocket/Model/License.swift: -------------------------------------------------------------------------------- 1 | // 2 | // License.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/12. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct License { 12 | 13 | var titles: [String] = [] 14 | 15 | init?() { 16 | guard let dict: [String : Any] = parsePlist(FileName.licenses) else { 17 | return nil 18 | } 19 | guard let licenses = dict["PreferenceSpecifiers"] as? [[String: String]] else { 20 | return nil 21 | } 22 | titles = licenses.flatMap { $0["Title"] } 23 | } 24 | 25 | 26 | private func parsePlist(_ fileName: FileName) -> [String: Any]? { 27 | guard let filePath: URL = Bundle.main.url(forResource: fileName.rawValue, withExtension: "plist") else { 28 | return nil 29 | } 30 | do { 31 | let data = try Data(contentsOf: filePath) 32 | return try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] 33 | } 34 | catch { 35 | return nil 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /QiitaPocket/Model/LicenseDetail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LicenseDetail.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/07/17. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct LicenseDetail { 12 | 13 | var text: String = "" 14 | 15 | init?(title: String) { 16 | guard let dict: [String : Any] = parsePlist(title) else { 17 | return nil 18 | } 19 | guard let arr = dict["PreferenceSpecifiers"] as? [[String: String]] else { 20 | return nil 21 | } 22 | guard let text = arr.first?["FooterText"] else { 23 | return nil 24 | } 25 | 26 | self.text = text 27 | } 28 | 29 | private func parsePlist(_ fileName: String) -> [String: Any]? { 30 | guard let filePath: URL = Bundle.main.url(forResource: fileName, withExtension: "plist") else { 31 | return nil 32 | } 33 | do { 34 | let data = try Data(contentsOf: filePath) 35 | return try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] 36 | } 37 | catch { 38 | return nil 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /QiitaPocket/Model/SearchHistory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchHistory.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/20. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct SearchHistory { 12 | 13 | var tags: [String] { 14 | return UserSettings.getSearchHistory() 15 | } 16 | 17 | private let max = 10 18 | 19 | init() {} 20 | 21 | 22 | func add(tag: String) { 23 | if tag.isEmpty { return } 24 | 25 | var tags = self.tags 26 | if let _ = tags.index(of: tag) { return } // 重複して登録させない 27 | 28 | tags.insert(tag, at: 0) 29 | 30 | if tags.count > max { 31 | tags.removeLast() 32 | } 33 | UserSettings.setSearchHistory(tags: tags) 34 | } 35 | 36 | func delete(index: Int) { 37 | var tags = self.tags 38 | tags.remove(at: index) 39 | UserSettings.setSearchHistory(tags: tags) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /QiitaPocket/Model/SearchPeriod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchPeriod.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/09/04. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum SearchPeriod: String { 12 | case week 13 | case month 14 | } 15 | -------------------------------------------------------------------------------- /QiitaPocket/Model/SearchType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchType.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/18. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum SearchType: String { 12 | case rank 13 | case recent 14 | } 15 | -------------------------------------------------------------------------------- /QiitaPocket/Util/UserSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserSettings.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/02. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // TODO: Protocol + Structにする 12 | class UserSettings { 13 | 14 | // 検索キーワード 15 | static func getcurrentTag() -> String { 16 | UserDefaults.standard.register(defaults: ["CurrentsearchTag": ""]) 17 | return UserDefaults.standard.string(forKey: "CurrentsearchTag")! 18 | } 19 | static func setcurrentTag(name: String) { 20 | UserDefaults.standard.set(name, forKey: "CurrentsearchTag") 21 | } 22 | 23 | // 検索履歴 24 | static func getSearchHistory() -> [String] { 25 | let strArray: [String] = [] 26 | UserDefaults.standard.register(defaults: ["SearchHistory": strArray]) 27 | return UserDefaults.standard.stringArray(forKey: "SearchHistory")! 28 | } 29 | static func setSearchHistory(tags: [String]) { 30 | UserDefaults.standard.set(tags, forKey: "SearchHistory") 31 | UserDefaults.standard.synchronize() 32 | } 33 | 34 | // 検索タイプ 35 | static func getSearchType() -> SearchType { 36 | UserDefaults.standard.register(defaults: ["SearchType": "rank"]) 37 | guard let rawValue = UserDefaults.standard.string(forKey: "SearchType") else { 38 | return SearchType.rank 39 | } 40 | guard let searchType = SearchType(rawValue: rawValue) else { 41 | return SearchType.rank 42 | } 43 | return searchType 44 | } 45 | static func setSearchType(_ searchType: SearchType) { 46 | UserDefaults.standard.set(searchType.rawValue, forKey: "SearchType") 47 | } 48 | 49 | // 検索期間 50 | static func getSearchPeriod() -> SearchPeriod { 51 | UserDefaults.standard.register(defaults: ["SearchPeriod": SearchPeriod.week.rawValue]) 52 | guard 53 | let rawvalue = UserDefaults.standard.string(forKey: "SearchPeriod"), 54 | let searchPeriod = SearchPeriod(rawValue: rawvalue) 55 | else { 56 | return SearchPeriod.week 57 | } 58 | return searchPeriod 59 | } 60 | static func setSearchPeriod(_ searchPeriod: SearchPeriod) { 61 | UserDefaults.standard.set(searchPeriod.rawValue, forKey: "SearchPeriod") 62 | } 63 | 64 | // delete 65 | static func delete(for key: String) { 66 | UserDefaults.standard.removeObject(forKey: key) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /QiitaPocket/Util/Util.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Util.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/01/29. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Util { 12 | 13 | static func setDisplayDate(str: String, format: String) -> String { 14 | let inFormatter = DateFormatter() 15 | inFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 16 | guard let date = inFormatter.date(from: str) else { 17 | return "" 18 | } 19 | 20 | let outFormatter = DateFormatter() 21 | outFormatter.dateFormat = format 22 | return outFormatter.string(from: date) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /QiitaPocket/View/ArchiveTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArchiveTableViewCell.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/13. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebImage 11 | import RxSwift 12 | 13 | final class ArchiveTableViewCell: UITableViewCell, ArticleCellType { 14 | 15 | @IBOutlet weak var articleView: ArticleView! 16 | 17 | weak var delegate: ArticleCellDelegate? 18 | private var recycleBag = DisposeBag() 19 | 20 | 21 | var article: Article! { 22 | didSet { 23 | configureCell(article: article) 24 | articleView.dateLabel.text = "\(article.formattedUpdatedAt) 保存" 25 | articleView.actionButton.rx.tap 26 | .bindNext { [weak self] in 27 | guard let `self` = self else { return } 28 | 29 | self.articleView.actionButton.isSelected = true 30 | self.delegate?.didTapActionButton(on: self) 31 | } 32 | .addDisposableTo(recycleBag) 33 | } 34 | } 35 | 36 | override func prepareForReuse() { 37 | super.prepareForReuse() 38 | recycleBag = DisposeBag() 39 | self.articleView.actionButton.isSelected = false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /QiitaPocket/View/ArchiveTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /QiitaPocket/View/ArticleCellType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCellType.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/27. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ArticleCellDelegate: class { 12 | func didTapActionButton(on cell: UITableViewCell) 13 | } 14 | 15 | protocol ArticleCellType: class { 16 | weak var articleView: ArticleView! { get } 17 | func configureCell(article: Article) 18 | } 19 | 20 | extension ArticleCellType where Self: UITableViewCell { 21 | func configureCell(article: Article) { 22 | articleView.titleLabel.text = article.title 23 | var tags: String = "" 24 | article.tags.forEach { tags += "\($0.name) " } 25 | articleView.tagLabel.text = tags 26 | articleView.authorID.text = article.author 27 | let url = URL(string: article.profile_image_url) 28 | articleView.profileImageView.sd_setImage(with: url) 29 | articleView.stockCount.text = "\(article.stockCount)" 30 | articleView.saveState = article.saveStateType 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /QiitaPocket/View/ArticleTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleTableViewCell.swift 3 | // qiitareader 4 | // 5 | // Created by hirothings on 2016/10/23. 6 | // Copyright © 2016年 hiroshings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebImage 11 | import RxSwift 12 | import RxCocoa 13 | 14 | final class ArticleTableViewCell: UITableViewCell, SwipeCellType, ArticleCellType { 15 | 16 | @IBOutlet weak var articleView: ArticleView! 17 | @IBOutlet weak var readLaterIcon: UIImageView! 18 | @IBOutlet weak var readLaterIconView: UIImageView! 19 | 20 | var swipeGesture = UIPanGestureRecognizer() 21 | var swipeIndexPath: IndexPath = IndexPath() 22 | var isSwiping = false 23 | 24 | weak var delegate: SwipeCellDelegate? 25 | private var recycleBag = DisposeBag() 26 | 27 | var article: Article! { 28 | didSet { 29 | configureCell(article: article) 30 | 31 | articleView.dateLabel.text = "\(article.publishedAt) 投稿" 32 | swipeGesture.rx.event.bindNext { [weak self] (gesture: UIPanGestureRecognizer) in 33 | guard let `self` = self else { return } 34 | self.onRightSwipe(gesture, iconView: self.readLaterIconView) 35 | } 36 | .addDisposableTo(recycleBag) 37 | 38 | articleView.actionButton.rx.tap 39 | .bindNext { [weak self] in 40 | guard let `self` = self else { return } 41 | 42 | self.articleView.actionButton.isSelected = true 43 | self.delegate?.didSwipe(cell: self) 44 | } 45 | .addDisposableTo(recycleBag) 46 | 47 | if let rank = article.rank.value { 48 | articleView.rankBadgeView.isHidden = false 49 | articleView.rankLabel.text = "\(rank)" 50 | switch rank { 51 | case 1: 52 | articleView.rankBGImageView.tintColor = UIColor.rankGold 53 | case 2: 54 | articleView.rankBGImageView.tintColor = UIColor.rankSilver 55 | case 3: 56 | articleView.rankBGImageView.tintColor = UIColor.rankBronse 57 | default: 58 | articleView.rankBGImageView.tintColor = UIColor.disabled 59 | } 60 | } 61 | else { 62 | articleView.rankBadgeView.isHidden = true 63 | } 64 | 65 | articleView.actionButton.isSelected = article.hasSaved 66 | } 67 | } 68 | 69 | 70 | override func awakeFromNib() { 71 | super.awakeFromNib() 72 | swipeGesture.delegate = self 73 | self.articleView.addGestureRecognizer(swipeGesture) 74 | } 75 | 76 | override func prepareForReuse() { 77 | super.prepareForReuse() 78 | recycleBag = DisposeBag() 79 | self.articleView.actionButton.isSelected = false 80 | } 81 | 82 | override func setHighlighted(_ highlighted: Bool, animated: Bool) { 83 | super.setHighlighted(true, animated: true) 84 | if highlighted { 85 | self.articleView.backgroundColor = UIColor.bg 86 | } 87 | else { 88 | self.articleView.backgroundColor = UIColor.white 89 | self.contentView.backgroundColor = UIColor.theme 90 | } 91 | } 92 | 93 | override func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 94 | return true 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /QiitaPocket/View/ArticleTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /QiitaPocket/View/ArticleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleView.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/04. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ArticleView: UIView { 12 | 13 | @IBOutlet weak var dateLabel: UILabel! 14 | @IBOutlet weak var profileImageView: UIImageView! 15 | @IBOutlet weak var authorID: UILabel! 16 | @IBOutlet weak var titleLabel: UILabel! 17 | @IBOutlet weak var tagLabel: UILabel! 18 | @IBOutlet weak var stockCount: UILabel! 19 | @IBOutlet weak var actionButton: UIButton! 20 | @IBOutlet weak var rankBadgeView: UIView! 21 | @IBOutlet weak var rankLabel: UILabel! 22 | 23 | var rankBGImageView: UIImageView = { 24 | let bgImage = #imageLiteral(resourceName: "ic-rankBadge") .withRenderingMode(.alwaysTemplate) 25 | let rankBGImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 15, height: 15)) 26 | rankBGImageView.image = bgImage 27 | return rankBGImageView 28 | }() 29 | 30 | var saveState: SaveState = .none { 31 | willSet(state) { 32 | switch state { 33 | case .none: 34 | self.actionButton.setImage(#imageLiteral(resourceName: "ic-read-later_disabled"), for: .normal) 35 | self.actionButton.setImage(#imageLiteral(resourceName: "ic-read-later"), for: .selected) 36 | case .readLater: 37 | self.actionButton.setImage(#imageLiteral(resourceName: "ic-check_disabled"), for: .normal) 38 | self.actionButton.setImage(#imageLiteral(resourceName: "ic-check"), for: .selected) 39 | case .archive: 40 | self.actionButton.setImage(#imageLiteral(resourceName: "ic-delete"), for: .normal) 41 | self.actionButton.setImage(#imageLiteral(resourceName: "ic-delete_on"), for: .selected) 42 | } 43 | } 44 | } 45 | 46 | override init(frame: CGRect) { 47 | super.init(frame: frame) 48 | initview() 49 | } 50 | 51 | required init?(coder aDecoder: NSCoder) { 52 | super.init(coder: aDecoder) 53 | initview() 54 | } 55 | 56 | private func initview() { 57 | let view = Bundle.main.loadNibNamed("ArticleView", owner: self, options: nil)!.first as! UIView 58 | addSubview(view) 59 | 60 | // profile画像を丸くする 61 | profileImageView.layer.cornerRadius = profileImageView.frame.size.width * 0.5 62 | profileImageView.clipsToBounds = true 63 | 64 | // rankViewは非表示にしておく 65 | rankBadgeView.isHidden = true 66 | 67 | // 最背面にランキング画像を置く 68 | rankBadgeView.addSubview(rankBGImageView) 69 | rankBadgeView.sendSubview(toBack: rankBGImageView) 70 | 71 | // カスタムViewのサイズを自分自身と同じサイズにする 72 | view.translatesAutoresizingMaskIntoConstraints = false 73 | self.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0).isActive = true 74 | self.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0).isActive = true 75 | self.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0).isActive = true 76 | self.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0).isActive = true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /QiitaPocket/View/ReadLaterTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadLaterTableViewCell.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/13. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebImage 11 | import RxSwift 12 | 13 | final class ReadLaterTableViewCell: UITableViewCell, SwipeCellType, ArticleCellType { 14 | 15 | @IBOutlet weak var checkIconView: UIImageView! 16 | @IBOutlet weak var articleView: ArticleView! 17 | 18 | var swipeGesture = UIPanGestureRecognizer() 19 | var swipeIndexPath: IndexPath = IndexPath() 20 | var isSwiping = false 21 | 22 | weak var delegate: SwipeCellDelegate? 23 | private var recycleBag = DisposeBag() 24 | 25 | 26 | var article: Article! { 27 | didSet { 28 | configureCell(article: article) 29 | articleView.dateLabel.text = "\(article.formattedUpdatedAt) 保存" 30 | 31 | swipeGesture.rx.event.bindNext { [weak self] (gesture: UIPanGestureRecognizer) in 32 | guard let `self` = self else { return } 33 | self.onRightSwipe(gesture, iconView: self.checkIconView) 34 | } 35 | .addDisposableTo(recycleBag) 36 | 37 | articleView.actionButton.rx.tap 38 | .bindNext { [weak self] in 39 | guard let `self` = self else { return } 40 | 41 | self.articleView.actionButton.isSelected = true 42 | self.delegate?.didSwipe(cell: self) 43 | } 44 | .addDisposableTo(recycleBag) 45 | } 46 | } 47 | 48 | override func awakeFromNib() { 49 | super.awakeFromNib() 50 | swipeGesture.delegate = self 51 | self.articleView.addGestureRecognizer(swipeGesture) 52 | } 53 | 54 | override func prepareForReuse() { 55 | super.prepareForReuse() 56 | recycleBag = DisposeBag() 57 | self.articleView.actionButton.isSelected = false 58 | } 59 | 60 | override func setHighlighted(_ highlighted: Bool, animated: Bool) { 61 | super.setHighlighted(true, animated: true) 62 | if highlighted { 63 | self.articleView.backgroundColor = UIColor.bg 64 | } 65 | else { 66 | self.articleView.backgroundColor = UIColor.white 67 | self.contentView.backgroundColor = UIColor.readLater 68 | } 69 | } 70 | 71 | override func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 72 | return true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /QiitaPocket/View/ReadLaterTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /QiitaPocket/View/SearchHistoryTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchHistoryTableViewCell.swift 3 | // QiitaPocket 4 | // 5 | // Created by PIVOT on 2017/05/02. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | protocol SearchHistoryCellDelegate: class { 14 | func didTapDeleteBtn(on cell: UITableViewCell) 15 | } 16 | 17 | 18 | class SearchHistoryTableViewCell: UITableViewCell { 19 | 20 | weak var delegate: SearchHistoryCellDelegate? 21 | 22 | @IBOutlet weak var titleLabel: UILabel! 23 | @IBOutlet weak var deleteBtn: UIButton! 24 | 25 | private var recycleBag = DisposeBag() 26 | 27 | override func awakeFromNib() { 28 | super.awakeFromNib() 29 | deleteBtn.rx.tap.bindNext { [weak self] in 30 | guard let `self` = self else { return } 31 | self.delegate?.didTapDeleteBtn(on: self) 32 | } 33 | .addDisposableTo(recycleBag) 34 | } 35 | 36 | override func prepareForReuse() { 37 | super.prepareForReuse() 38 | recycleBag = DisposeBag() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /QiitaPocket/View/SwipeCellType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeCellType.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/04. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol SwipeCellDelegate: class { 12 | func isSwipingCell(isSwiping: Bool) 13 | func didSwipe(cell: UITableViewCell) 14 | } 15 | 16 | protocol SwipeCellType: class { 17 | weak var articleView: ArticleView! { get } 18 | weak var delegate: SwipeCellDelegate? { get } 19 | var swipeGesture: UIPanGestureRecognizer { get } 20 | var swipeIndexPath: IndexPath { get set } 21 | var isSwiping: Bool { get set } 22 | func onRightSwipe(_ gesture: UIPanGestureRecognizer, iconView: UIImageView) 23 | } 24 | 25 | extension SwipeCellType where Self: UITableViewCell { 26 | 27 | func onRightSwipe(_ gesture: UIPanGestureRecognizer, iconView: UIImageView) { 28 | let translation = gesture.translation(in: self) 29 | let swipeThreshold: CGFloat = UIScreen.main.bounds.width * 0.35 30 | 31 | switch gesture.state { 32 | case .began: 33 | break 34 | case .changed: 35 | // 左端より先にはスワイプさせない 36 | if translation.x < 0 { return } 37 | 38 | // スワイプ中、cellを移動させる 39 | if isSwiping == true { 40 | self.articleView.frame.origin.x = translation.x 41 | // アイコンの拡大・縮小 42 | let ratio = translation.x / swipeThreshold 43 | if 1.0 < ratio { return } 44 | iconView.alpha = ratio 45 | iconView.transform = CGAffineTransform(scaleX: ratio, y: ratio) 46 | return 47 | } 48 | 49 | // Y軸へのトランジションが閾値以内の場合、セルを右スワイプ中とみなす 50 | if abs(translation.y) < 5 && translation.y < translation.x { 51 | isSwiping = true 52 | self.delegate?.isSwipingCell(isSwiping: isSwiping) 53 | } 54 | 55 | case .ended: 56 | if (swipeThreshold) < translation.x { 57 | UIView.animate( 58 | withDuration: 0.1, 59 | animations: { [unowned self] in 60 | self.articleView.frame.origin.x = UIScreen.main.bounds.width 61 | }, 62 | completion: { [unowned self] _ in 63 | self.delegate?.didSwipe(cell: self) 64 | }) 65 | } 66 | else { 67 | UIView.animate(withDuration: 0.1, animations: { [unowned self] in 68 | self.articleView.frame.origin.x = 0 69 | }) 70 | } 71 | isSwiping = false 72 | self.delegate?.isSwipingCell(isSwiping: isSwiping) 73 | default: 74 | break 75 | } 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/ArchiveViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArchiveViewController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/27. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RealmSwift 13 | import XLPagerTabStrip 14 | import SafariServices 15 | 16 | class ArchiveViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, IndicatorInfoProvider, ArticleCellDelegate, BannerViewType { 17 | 18 | @IBOutlet weak var tableView: UITableView! 19 | 20 | var articles: Results
= { 21 | return ArticleManager.getArchives() 22 | }() 23 | 24 | var notificationToken: NotificationToken? 25 | 26 | private let bag = DisposeBag() 27 | 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | tableView.estimatedRowHeight = 103.0 33 | tableView.rowHeight = UITableViewAutomaticDimension 34 | tableView.separatorInset = UIEdgeInsets.zero 35 | 36 | let nib: UINib = UINib(nibName: "ArchiveTableViewCell", bundle: nil) 37 | self.tableView.register(nib, forCellReuseIdentifier: "CustomCell") 38 | 39 | tableView.delegate = self 40 | tableView.dataSource = self 41 | 42 | // Realm更新時、reloadDataする 43 | notificationToken = articles.addNotificationBlock { [weak self] (change: RealmCollectionChange) in 44 | guard let tableView = self?.tableView else { return } 45 | 46 | switch change { 47 | case .initial: 48 | tableView.reloadData() 49 | case .update(_, deletions: let deletions, insertions: let insertions, modifications: let modifications): 50 | tableView.beginUpdates() 51 | tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), 52 | with: .automatic) 53 | tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), 54 | with: .automatic) 55 | tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), 56 | with: .automatic) 57 | tableView.endUpdates() 58 | case .error(let error): 59 | // TODO: エラー処理 60 | fatalError("\(error)") 61 | break 62 | } 63 | } 64 | setupBannerView(containerView: self.view) 65 | } 66 | 67 | override func viewWillAppear(_ animated: Bool) { 68 | super.viewWillAppear(animated) 69 | tableView.tableFooterView = UIView(frame: CGRect.zero) 70 | } 71 | 72 | deinit { 73 | notificationToken?.stop() 74 | } 75 | 76 | 77 | // MARK: - IndicatorInfoProvider 78 | 79 | func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo { 80 | return IndicatorInfo(title: "アーカイブ") 81 | } 82 | 83 | 84 | // MARK: - TableView Delegate 85 | 86 | /// tableViewの行数を指定 87 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 88 | return articles.count 89 | } 90 | 91 | /// tableViewのcellを生成 92 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 93 | let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! ArchiveTableViewCell 94 | cell.article = articles[indexPath.row] 95 | cell.delegate = self 96 | 97 | return cell 98 | } 99 | 100 | /// tableViewタップ時webViewに遷移する 101 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 102 | let article = articles[indexPath.row] 103 | 104 | guard let url = URL(string: article.url) else { return } 105 | let safariVC = SFSafariViewController(url: url) 106 | safariVC.modalPresentationStyle = .popover 107 | self.present(safariVC, animated: true, completion: nil) 108 | 109 | tableView.deselectRow(at: indexPath, animated: true) 110 | } 111 | 112 | 113 | // MARK: - ArticleCellDelegate 114 | 115 | func didTapActionButton(on cell: UITableViewCell) { 116 | guard let indexPath = tableView.indexPath(for: cell) else { return } 117 | let article = articles[indexPath.row] 118 | ArticleManager.delete(article: article) 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/ArticleListNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleListNavigationController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/03/09. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | class ArticleListNavigationController: UINavigationController { 14 | 15 | var searchBar = UISearchBar() 16 | private let bag = DisposeBag() 17 | 18 | private let settingButton: UIBarButtonItem = { 19 | var posY: CGFloat 20 | if #available(iOS 11.0, *) { 21 | posY = 6 22 | } 23 | else { 24 | posY = 0 25 | } 26 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) 27 | let button = UIButton(frame: CGRect(x: 0, y: posY, width: 44, height: 44)) 28 | let image = #imageLiteral(resourceName: "ic-setting") 29 | button.setImage(image, for: .normal) 30 | button.imageEdgeInsets = UIEdgeInsetsMake(14.0, 14.0, 14.0, 14.0) 31 | button.imageView?.frame = CGRect(x: 14, y: 14, width: 16.0, height: 16.0) 32 | button.addTarget(self, action: #selector(didTapSettingButton(_:)), for: .touchUpInside) 33 | view.addSubview(button) 34 | return UIBarButtonItem(customView: view) 35 | }() 36 | 37 | private let logoImageItem: UIBarButtonItem = { 38 | var posY: CGFloat 39 | if #available(iOS 11.0, *) { 40 | posY = 12 41 | } 42 | else { 43 | posY = 8 44 | } 45 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 32, height: 44)) 46 | let logoImageView = UIImageView(image: #imageLiteral(resourceName: "logo")) 47 | logoImageView.frame = CGRect(x: 0, y: posY, width: 32, height: 32) 48 | logoImageView.contentMode = .scaleAspectFit 49 | view.addSubview(logoImageView) 50 | return UIBarButtonItem(customView: view) 51 | }() 52 | 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | setupSearchBar() 57 | setupLogoImage() 58 | setupSettingButton() 59 | } 60 | 61 | func unsetSettingButton() { 62 | navigationBar.topItem?.rightBarButtonItems?.removeAll() 63 | } 64 | 65 | func setupSettingButton() { 66 | let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) 67 | spacer.width = -10 68 | navigationBar.topItem?.rightBarButtonItems = [spacer, settingButton] 69 | } 70 | 71 | private func setupLogoImage() { 72 | let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) 73 | spacer.width = -10.0 74 | navigationBar.topItem?.leftBarButtonItems = [spacer, logoImageItem] 75 | } 76 | 77 | private func setupSearchBar() { 78 | searchBar.placeholder = "キーワードを入力" 79 | searchBar.showsCancelButton = false 80 | searchBar.autocapitalizationType = .none 81 | searchBar.keyboardType = .default 82 | searchBar.tintColor = UIColor.gray 83 | searchBar.text = UserSettings.getcurrentTag() 84 | searchBar.enablesReturnKeyAutomatically = false 85 | for subView in searchBar.subviews { 86 | for secondSubView in subView.subviews { 87 | if secondSubView is UITextField { 88 | secondSubView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.3) 89 | break 90 | } 91 | } 92 | } 93 | self.navigationBar.topItem?.titleView = searchBar 94 | } 95 | 96 | @objc func didTapSettingButton(_ sender: UITapGestureRecognizer) { 97 | let otherNVC = self.storyboard!.instantiateViewController(withIdentifier: "OtherNavigationController") as! OtherNavigationController 98 | self.present(otherNVC, animated: true, completion: nil) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/ArticleListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleListViewController.swift 3 | // 4 | // 5 | // Created by hirothings on 2016/05/04. 6 | // 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import SafariServices 13 | 14 | 15 | class ArticleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, SwipeCellDelegate, UISearchBarDelegate { 16 | 17 | @IBOutlet weak var tableView: UITableView! 18 | @IBOutlet weak var topIndicatorView: UIActivityIndicatorView! 19 | @IBOutlet weak var bottomIndicatorView: UIActivityIndicatorView! 20 | @IBOutlet weak var bottomView: UIView! 21 | @IBOutlet weak var noneDataLabel: UILabel! 22 | 23 | var articles: [Article] = [] 24 | var refreshControll = UIRefreshControl() 25 | 26 | private var viewModel: ArticleListViewModel! 27 | private var fetchTrigger = PublishSubject() 28 | private var searchArticleVC = SearchArticleViewController() 29 | private var searchBar: UISearchBar! 30 | private let bag = DisposeBag() 31 | private var nvc: ArticleListNavigationController! 32 | 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | let nib: UINib = UINib(nibName: "ArticleTableViewCell", bundle: nil) 38 | tableView.register(nib, forCellReuseIdentifier: "ArticleTableViewCell") 39 | 40 | viewModel = ArticleListViewModel(fetchTrigger: fetchTrigger) 41 | 42 | tableView.estimatedRowHeight = 103.0 43 | tableView.rowHeight = UITableViewAutomaticDimension 44 | tableView.separatorInset = UIEdgeInsets.zero 45 | tableView.sectionHeaderHeight = 30.0 46 | tableView.delegate = self 47 | tableView.dataSource = self 48 | 49 | tableView.isHidden = true 50 | noneDataLabel.isHidden = true 51 | 52 | nvc = self.navigationController as! ArticleListNavigationController 53 | searchBar = nvc.searchBar 54 | searchBar.delegate = self 55 | 56 | tableView.refreshControl = refreshControll 57 | 58 | bottomIndicatorView.hidesWhenStopped = true 59 | topIndicatorView.hidesWhenStopped = true 60 | 61 | 62 | // bind 63 | 64 | tableView.rx.reachedBottom 65 | .asDriver() 66 | .drive(viewModel.loadNextPageTrigger) 67 | .disposed(by: bag) 68 | 69 | refreshControll.rx.controlEvent(.valueChanged) 70 | .startWith(()) 71 | .flatMap { Observable.just(UserSettings.getcurrentTag()) } 72 | .bind(to: fetchTrigger) 73 | .addDisposableTo(bag) 74 | 75 | viewModel.isLoading 76 | .do(onNext: { [weak self] in 77 | self?.noneDataLabel.isHidden = $0 78 | }) 79 | .drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible) 80 | .addDisposableTo(bag) 81 | 82 | viewModel.isLoading 83 | .drive(bottomIndicatorView.rx.isAnimating) 84 | .addDisposableTo(bag) 85 | 86 | viewModel.isLoading 87 | .drive(topIndicatorView.rx.isAnimating) 88 | .addDisposableTo(bag) 89 | 90 | viewModel.hasData.asObservable() 91 | .skip(1) 92 | .bind(to: noneDataLabel.rx.isHidden) 93 | .addDisposableTo(bag) 94 | 95 | viewModel.firstLoad 96 | .bind(onNext: { [unowned self] articles in 97 | print("first load done") 98 | 99 | self.articles = articles 100 | self.setUpBottomView() 101 | self.tableView.reloadData() 102 | let topIndexPath = IndexPath(row: 0, section: 0) 103 | self.tableView.scrollToRow(at: topIndexPath, at: .top, animated: false) 104 | self.tableView.isHidden = false 105 | self.refreshControll.endRefreshing() 106 | }) 107 | .addDisposableTo(bag) 108 | 109 | viewModel.additionalLoad 110 | .bind(onNext: { [unowned self] articles in 111 | print("additional load done") 112 | 113 | let indexPath: [IndexPath] = Array(self.articles.count.. Int { 132 | return articles.count 133 | } 134 | 135 | /// tableViewのcellを生成 136 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 137 | let cell = tableView.dequeueReusableCell(withIdentifier: "ArticleTableViewCell", for: indexPath) as! ArticleTableViewCell 138 | cell.article = articles[indexPath.row] 139 | cell.delegate = self 140 | 141 | return cell 142 | } 143 | 144 | 145 | // MARK: - TableView Delegate 146 | 147 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 148 | let label: UILabel = { 149 | let lb = UILabel(frame: CGRect(x: 10.0, y: 0.0, width: UIScreen.main.bounds.width, height: 30.0)) 150 | lb.text = viewModel.searchTitle 151 | lb.textColor = UIColor.white 152 | lb.font = UIFont.boldSystemFont(ofSize: 12.0) 153 | return lb 154 | }() 155 | 156 | let view: UIView = { 157 | let v = UIView() 158 | v.backgroundColor = viewModel.titleColor 159 | return v 160 | }() 161 | 162 | view.addSubview(label) 163 | return view 164 | } 165 | 166 | /// tableViewタップ時webViewに遷移する 167 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 168 | let article = articles[indexPath.row] 169 | 170 | guard let url = URL(string: article.url) else { return } 171 | let safariVC = SFSafariViewController(url: url) 172 | safariVC.modalPresentationStyle = .popover 173 | self.present(safariVC, animated: true, completion: nil) 174 | 175 | tableView.deselectRow(at: indexPath, animated: true) 176 | } 177 | 178 | 179 | // MARK: - SwipeCellDelegate 180 | 181 | func isSwipingCell(isSwiping: Bool) { 182 | tableView.panGestureRecognizer.isEnabled = !(isSwiping) 183 | } 184 | 185 | func didSwipe(cell: UITableViewCell) { 186 | tableView.beginUpdates() 187 | guard let indexPath = tableView.indexPath(for: cell) else { return } 188 | let article = articles[indexPath.row] 189 | ArticleManager.add(readLater: article) // Realmに記事を保存 190 | 191 | articles.remove(at: indexPath.row) 192 | tableView.deleteRows(at: [indexPath], with: .automatic) 193 | tableView.endUpdates() 194 | } 195 | 196 | 197 | // MARK: - Private Method 198 | 199 | /// 検索ViewControllerをセット 200 | private func setupSearchArticleVC() { 201 | searchArticleVC = self.storyboard!.instantiateViewController(withIdentifier: "SearchArticleViewController") as! SearchArticleViewController 202 | self.addChildViewController(searchArticleVC) 203 | self.view.addSubview(searchArticleVC.view) 204 | searchArticleVC.didMove(toParentViewController: self) 205 | 206 | // 検索履歴タップ時のイベント 207 | searchArticleVC.didSelectSearchHistory 208 | .subscribe(onNext: { [unowned self] (tag: String) in 209 | self.searchBar.text = tag 210 | self.fetchTrigger.onNext(tag) 211 | self.searchBar.endEditing(true) 212 | self.searchBar.showsCancelButton = false 213 | self.removeSearchArticleVC() 214 | self.tableView.isHidden = true 215 | }) 216 | .addDisposableTo(bag) 217 | 218 | nvc.unsetSettingButton() 219 | } 220 | 221 | /// 検索ViewControllerを削除 222 | private func removeSearchArticleVC() { 223 | searchArticleVC.willMove(toParentViewController: self) 224 | searchArticleVC.view.removeFromSuperview() 225 | searchArticleVC.removeFromParentViewController() 226 | nvc.setupSettingButton() 227 | } 228 | 229 | private func showAlert(message: String) { 230 | let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) 231 | let defAction = UIAlertAction(title: "OK", style: .default, handler: nil) 232 | alert.addAction(defAction) 233 | self.present(alert, animated: true, completion: nil) 234 | } 235 | 236 | private func setUpBottomView() { 237 | let rect = CGRect(x: bottomView.bounds.origin.x, 238 | y: bottomView.bounds.origin.y, 239 | width: bottomView.bounds.width, 240 | height: viewModel.bottomViewHeight) 241 | self.bottomView.frame = rect 242 | } 243 | 244 | 245 | // MARK: - UISearchBarDelegate 246 | 247 | func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { 248 | setupSearchArticleVC() 249 | searchBar.showsCancelButton = true 250 | } 251 | 252 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 253 | removeSearchArticleVC() 254 | searchBar.endEditing(true) 255 | searchBar.showsCancelButton = false 256 | } 257 | 258 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 259 | removeSearchArticleVC() 260 | searchBar.endEditing(true) 261 | searchBar.showsCancelButton = false 262 | fetchTrigger.onNext(searchBar.text!) 263 | self.tableView.isHidden = true 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/ArticleTableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleTableViewDataSource.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/05/05. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class ArticleTableViewDataSource: NSObject, UITableViewDataSource, RxTableViewDataSourceType, SwipeCellDelegate, UITableViewDelegate { 15 | 16 | typealias Element = [Article] 17 | private var articles: Element = [] 18 | 19 | let isSwipingCell = PublishSubject() 20 | let didSwipeCell = PublishSubject() 21 | let didTapTableRow = PublishSubject() 22 | 23 | func tableView(_ tableView: UITableView, observedEvent: Event) { 24 | if case .next(let articles) = observedEvent { 25 | self.articles = articles 26 | tableView.reloadData() 27 | } 28 | } 29 | 30 | 31 | // MARK: - TableView DataSource 32 | 33 | /// tableViewの行数を指定 34 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | return articles.count 36 | } 37 | 38 | /// tableViewのcellを生成 39 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 40 | let cell = tableView.dequeueReusableCell(withIdentifier: "ArticleTableViewCell", for: indexPath) as! ArticleTableViewCell 41 | cell.article = articles[indexPath.row] 42 | cell.delegate = self 43 | 44 | return cell 45 | } 46 | 47 | 48 | // MARK: - TableView Delegate 49 | 50 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 51 | let searchType = UserSettings.getSearchType() 52 | let currentTag = UserSettings.getcurrentTag() 53 | 54 | var text: String 55 | switch searchType { 56 | case .rank: 57 | text = "週間ランキング" 58 | case .recent: 59 | text = "新着順" 60 | } 61 | 62 | if currentTag.isEmpty { 63 | text += ": すべて" 64 | } 65 | else { 66 | text += ": \(currentTag)" 67 | } 68 | 69 | let label: UILabel = { 70 | let lb = UILabel(frame: CGRect(x: 10.0, y: 0.0, width: UIScreen.main.bounds.width, height: 30.0)) 71 | lb.text = text 72 | lb.textColor = UIColor.white 73 | lb.font = UIFont.boldSystemFont(ofSize: 12.0) 74 | return lb 75 | }() 76 | 77 | let view: UIView = { 78 | let v = UIView() 79 | switch searchType { 80 | case .rank: 81 | v.backgroundColor = UIColor.rankGold 82 | case .recent: 83 | v.backgroundColor = UIColor.theme 84 | } 85 | return v 86 | }() 87 | 88 | view.addSubview(label) 89 | return view 90 | } 91 | 92 | 93 | // MARK: - SwipeCellDelegate 94 | 95 | func isSwipingCell(isSwiping: Bool) { 96 | isSwipingCell.onNext(isSwiping) 97 | } 98 | 99 | func didSwipe(cell: UITableViewCell) { 100 | didSwipeCell.onNext(cell) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/BannerViewType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BannerViewType.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/04/16. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import GoogleMobileAds 11 | 12 | protocol BannerViewType { 13 | func setupBannerView(containerView: UIView) 14 | } 15 | 16 | extension BannerViewType where Self: UIViewController { 17 | 18 | func setupBannerView(containerView: UIView) { 19 | let bannerView = GADBannerView() 20 | containerView.addSubview(bannerView) 21 | 22 | // Auto Layout 23 | bannerView.translatesAutoresizingMaskIntoConstraints = false 24 | bannerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true 25 | bannerView.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true 26 | bannerView.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true 27 | bannerView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true 28 | 29 | // google AD 30 | // TODO: ifDEBUG 31 | // bannerView.adUnitID = "ca-app-pub-3940256099942544/2934735716" 32 | bannerView.adUnitID = "ca-app-pub-8842953390661934/7450873200" 33 | bannerView.rootViewController = self 34 | let gadRequest = GADRequest() 35 | bannerView.load(gadRequest) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/LicenseDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LicenseDetailViewController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/04/15. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LicenseDetailViewController: UIViewController { 12 | 13 | let licenseText: String 14 | 15 | init?(title: String) { 16 | guard let licenseDetail = LicenseDetail(title: title) else { 17 | return nil 18 | } 19 | self.licenseText = licenseDetail.text 20 | super.init(nibName: nil, bundle: nil) 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | let scrollView = UIScrollView() 31 | let contentView = UIView() 32 | let label = UILabel() 33 | 34 | self.view.addSubview(scrollView) 35 | scrollView.addSubview(contentView) 36 | contentView.addSubview(label) 37 | 38 | // Layout 39 | 40 | scrollView.translatesAutoresizingMaskIntoConstraints = false 41 | scrollView.topAnchor.constraint(equalTo: self.view.layoutMarginsGuide.topAnchor).isActive = true 42 | scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true 43 | scrollView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true 44 | scrollView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true 45 | scrollView.backgroundColor = UIColor.bg 46 | 47 | contentView.translatesAutoresizingMaskIntoConstraints = false 48 | contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true 49 | contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true 50 | contentView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true 51 | contentView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true 52 | 53 | contentView.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true 54 | contentView.heightAnchor.constraint(equalTo: self.view.heightAnchor).priority = UILayoutPriority(rawValue: 250) 55 | contentView.backgroundColor = UIColor.white 56 | 57 | label.translatesAutoresizingMaskIntoConstraints = false 58 | label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10.0).isActive = true 59 | label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10.0).isActive = true 60 | label.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10.0).isActive = true 61 | label.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10.0).isActive = true 62 | 63 | 64 | label.numberOfLines = 0 65 | label.text = self.licenseText 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/LicensesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LicensesViewController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/04/08. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LicensesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 12 | 13 | @IBOutlet weak var tableView: UITableView! 14 | let license = License() 15 | var titles: [String] { 16 | return license?.titles ?? [] 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | tableView.separatorInset = UIEdgeInsets.zero 23 | tableView.delegate = self 24 | tableView.dataSource = self 25 | 26 | self.navigationItem.title = "Acknowledgements" 27 | self.tableView.backgroundColor = UIColor.bg 28 | } 29 | 30 | override func viewWillAppear(_ animated: Bool) { 31 | super.viewWillAppear(animated) 32 | tableView.tableFooterView = UIView(frame: CGRect.zero) 33 | } 34 | 35 | 36 | // MARK: - TableView Delegate 37 | 38 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 39 | return titles.count 40 | } 41 | 42 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 43 | let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell")! 44 | cell.textLabel?.text = titles[indexPath.row] 45 | cell.accessoryType = .disclosureIndicator 46 | return cell 47 | } 48 | 49 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 50 | let title: String = titles[indexPath.row] 51 | guard let licenseDetailVC = LicenseDetailViewController(title: title) else { 52 | return 53 | } 54 | self.navigationController?.pushViewController(licenseDetailVC, animated: true) 55 | tableView.deselectRow(at: indexPath, animated: true) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/MainTabBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainTabBarController.swift 3 | // qiitareader 4 | // 5 | // Created by hirothings on 2016/10/23. 6 | // Copyright © 2016年 hiroshings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MainTabBarController: UITabBarController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | guard let searchButton = tabBar.items?.first else { return } 17 | guard let readLaterButton = tabBar.items?.last else { return } 18 | 19 | // tabBar フォント調整 20 | let normalAttributes: [NSAttributedStringKey: Any] = [ 21 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 10), 22 | NSAttributedStringKey.foregroundColor: UIColor.disabled 23 | ] 24 | let selectedAttributes: [NSAttributedStringKey: Any] = [ 25 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 10), 26 | NSAttributedStringKey.foregroundColor: UIColor.theme 27 | ] 28 | searchButton.setTitleTextAttributes(normalAttributes, for: .normal) 29 | searchButton.setTitleTextAttributes(selectedAttributes, for: .selected) 30 | readLaterButton.setTitleTextAttributes(normalAttributes, for: .normal) 31 | readLaterButton.setTitleTextAttributes(selectedAttributes, for: .selected) 32 | 33 | // tabBar アイコン調整 34 | searchButton.image = #imageLiteral(resourceName: "ic-tab-search").withRenderingMode(.alwaysOriginal) 35 | readLaterButton.image = #imageLiteral(resourceName: "ic-tab-read-later").withRenderingMode(.alwaysOriginal) 36 | searchButton.selectedImage = #imageLiteral(resourceName: "ic-tab-search_selected").withRenderingMode(.alwaysOriginal) 37 | readLaterButton.selectedImage = #imageLiteral(resourceName: "ic-tab-read-later_selected").withRenderingMode(.alwaysOriginal) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/OtherNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherNavigationController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/04/02. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class OtherNavigationController: UINavigationController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | self.navigationBar.topItem?.leftBarButtonItem = UIBarButtonItem(title: "閉じる", style: .plain, target: self, action: #selector(didTapCloseButton)) 16 | } 17 | 18 | @objc func didTapCloseButton() { 19 | self.dismiss(animated: true, completion: nil) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/OtherTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherTableViewController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/04/02. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import SafariServices 13 | 14 | class OtherTableViewController: UITableViewController { 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | self.tableView.separatorInset = UIEdgeInsets.zero 19 | self.view.backgroundColor = UIColor.bg 20 | } 21 | 22 | override func viewWillAppear(_ animated: Bool) { 23 | super.viewWillAppear(animated) 24 | tableView.tableFooterView = UIView(frame: CGRect.zero) 25 | } 26 | 27 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 28 | 29 | switch indexPath.row { 30 | case 0: 31 | let licenseVC = self.storyboard?.instantiateViewController(withIdentifier: "LicensesViewController") 32 | self.navigationController?.pushViewController(licenseVC!, animated: true) 33 | case 1: 34 | guard let url = URL(string: "https://github.com/hirothings/qiita-pocket") else { return } 35 | let safariVC = SFSafariViewController(url: url) 36 | self.present(safariVC, animated: true, completion: nil) 37 | default: 38 | break 39 | } 40 | 41 | tableView.deselectRow(at: indexPath, animated: true) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/OtherViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherViewController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/04/16. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class OtherViewController: UIViewController, BannerViewType { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | self.navigationItem.title = "その他" 16 | setupBannerView(containerView: self.view) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/ReadLaterTabViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadLaterTabViewController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/27. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XLPagerTabStrip 11 | 12 | class ReadLaterTabViewController: ButtonBarPagerTabStripViewController { 13 | 14 | override func viewDidLoad() { 15 | settings.style.buttonBarBackgroundColor = .white 16 | settings.style.buttonBarItemBackgroundColor = .white 17 | settings.style.selectedBarBackgroundColor = .black 18 | settings.style.buttonBarItemFont = .boldSystemFont(ofSize: 14) 19 | settings.style.selectedBarHeight = 1.0 20 | settings.style.buttonBarMinimumLineSpacing = 0 21 | settings.style.buttonBarItemTitleColor = .black 22 | settings.style.buttonBarItemsShouldFillAvailableWidth = true 23 | settings.style.buttonBarLeftContentInset = 10 24 | settings.style.buttonBarRightContentInset = 10 25 | 26 | changeCurrentIndexProgressive = { (oldCell: ButtonBarViewCell?, newCell: ButtonBarViewCell?, progressPercentage: CGFloat, changeCurrentIndex: Bool, animated: Bool) -> Void in 27 | guard changeCurrentIndex == true else { return } 28 | 29 | oldCell?.label.textColor = UIColor.black.withAlphaComponent(0.6) 30 | newCell?.label.textColor = .black 31 | } 32 | 33 | super.viewDidLoad() 34 | 35 | buttonBarView.removeFromSuperview() 36 | navigationController?.navigationBar.addSubview(buttonBarView) 37 | self.containerView.bounces = false // cellスワイプと競合するので、bouncesを切る 38 | } 39 | 40 | override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { 41 | let readLaterVC = self.storyboard?.instantiateViewController(withIdentifier: "ReadLaterViewController") 42 | let archiveVC = self.storyboard?.instantiateViewController(withIdentifier: "ArchiveViewController") 43 | 44 | return [readLaterVC!, archiveVC!] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/ReadLaterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadLaterViewController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/27. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RealmSwift 13 | import XLPagerTabStrip 14 | import SafariServices 15 | 16 | final class ReadLaterViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, SwipeCellDelegate, IndicatorInfoProvider { 17 | 18 | @IBOutlet weak var tableView: UITableView! 19 | 20 | var articles: Results
= { 21 | return ArticleManager.getReadLaters() 22 | }() 23 | 24 | var notificationToken: NotificationToken? 25 | 26 | private let bag = DisposeBag() 27 | 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | tableView.estimatedRowHeight = 103.0 33 | tableView.rowHeight = UITableViewAutomaticDimension 34 | tableView.separatorInset = UIEdgeInsets.zero 35 | 36 | let nib: UINib = UINib(nibName: "ReadLaterTableViewCell", bundle: nil) 37 | self.tableView.register(nib, forCellReuseIdentifier: "CustomCell") 38 | 39 | tableView.delegate = self 40 | tableView.dataSource = self 41 | 42 | // Realm更新時、reloadDataする 43 | notificationToken = articles.addNotificationBlock { [weak self] (change: RealmCollectionChange) in 44 | guard let tableView = self?.tableView else { return } 45 | 46 | switch change { 47 | case .initial: 48 | tableView.reloadData() 49 | case .update(_, deletions: let deletions, insertions: let insertions, modifications: let modifications): tableView.beginUpdates() 50 | tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), 51 | with: .automatic) 52 | tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), 53 | with: .automatic) 54 | tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), 55 | with: .automatic) 56 | tableView.endUpdates() 57 | case .error(let error): 58 | // TODO: エラー処理 59 | fatalError("\(error)") 60 | break 61 | } 62 | } 63 | } 64 | 65 | override func viewWillAppear(_ animated: Bool) { 66 | super.viewWillAppear(animated) 67 | tableView.tableFooterView = UIView(frame: CGRect.zero) 68 | } 69 | 70 | deinit { 71 | notificationToken?.stop() 72 | } 73 | 74 | 75 | // MARK: - IndicatorInfoProvider 76 | 77 | func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo { 78 | return IndicatorInfo(title: "あとで読む") 79 | } 80 | 81 | 82 | // MARK: - TableView Delegate 83 | 84 | /// tableViewの行数を指定 85 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 86 | return articles.count 87 | } 88 | 89 | /// tableViewのcellを生成 90 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 91 | let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! ReadLaterTableViewCell 92 | cell.article = articles[indexPath.row] 93 | cell.delegate = self 94 | 95 | return cell 96 | } 97 | 98 | /// tableViewタップ時webViewに遷移する 99 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 100 | let article = articles[indexPath.row] 101 | 102 | guard let url = URL(string: article.url) else { return } 103 | let safariVC = SFSafariViewController(url: url) 104 | safariVC.modalPresentationStyle = .popover 105 | self.present(safariVC, animated: true, completion: nil) 106 | 107 | tableView.deselectRow(at: indexPath, animated: true) 108 | } 109 | 110 | 111 | // MARK: - SwipeCellDelegate 112 | 113 | func isSwipingCell(isSwiping: Bool) { 114 | tableView.panGestureRecognizer.isEnabled = !(isSwiping) 115 | } 116 | 117 | func didSwipe(cell: UITableViewCell) { 118 | guard let indexPath = tableView.indexPath(for: cell) else { return } 119 | let article = articles[indexPath.row] 120 | ArticleManager.add(archive: article) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /QiitaPocket/ViewController/SearchArticleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchArticleViewController.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2017/02/11. 6 | // Copyright © 2017年 hirothings. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | class SearchArticleViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, SearchHistoryCellDelegate { 14 | 15 | @IBOutlet weak var searchTypeSegment: UISegmentedControl! 16 | @IBOutlet weak var searchPeriodSegment: UISegmentedControl! 17 | @IBOutlet weak var tableView: UITableView! 18 | @IBOutlet weak var searchPeriodStackView: UIStackView! 19 | @IBOutlet weak var contentViewHeight: NSLayoutConstraint! 20 | @IBOutlet weak var searchPeriodTopMargin: NSLayoutConstraint! 21 | 22 | var didSelectSearchHistory = PublishSubject() 23 | 24 | private let bag = DisposeBag() 25 | private let searchHistory = SearchHistory() 26 | 27 | private var searchType: SearchType { 28 | return UserSettings.getSearchType() 29 | } 30 | private var searchPeriod: SearchPeriod { 31 | return UserSettings.getSearchPeriod() 32 | } 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | initSegmentValue() 38 | configureSearchTypeSegment() 39 | configureSearchPeriodSegment() 40 | 41 | tableView.delegate = self 42 | tableView.dataSource = self 43 | tableView.tableFooterView = UIView(frame: CGRect.zero) 44 | tableView.separatorInset = UIEdgeInsets.zero 45 | } 46 | 47 | override func viewDidLayoutSubviews() { 48 | let tablecellHeight: CGFloat = 44.0 49 | // tableView分の高さを追加する 50 | contentViewHeight.constant = tablecellHeight * CGFloat(searchHistory.tags.count) 51 | } 52 | 53 | 54 | // MARK: - TableView Delegate 55 | 56 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 57 | return searchHistory.tags.count 58 | } 59 | 60 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 61 | let cell = tableView.dequeueReusableCell(withIdentifier: "SearchHistoryTableViewCell", for: indexPath) as! SearchHistoryTableViewCell 62 | cell.titleLabel.text = searchHistory.tags[indexPath.row] 63 | cell.delegate = self 64 | return cell 65 | } 66 | 67 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 68 | let history = searchHistory.tags[indexPath.row] 69 | didSelectSearchHistory.onNext(history) 70 | } 71 | 72 | 73 | // MARK: - Private Method 74 | 75 | func initSegmentValue() { 76 | switch searchType { 77 | case .rank: 78 | searchTypeSegment.selectedSegmentIndex = 0 79 | case .recent: 80 | searchTypeSegment.selectedSegmentIndex = 1 81 | } 82 | 83 | switch searchPeriod { 84 | case .week: 85 | searchPeriodSegment.selectedSegmentIndex = 0 86 | case .month: 87 | searchPeriodSegment.selectedSegmentIndex = 1 88 | } 89 | } 90 | 91 | func configureSearchTypeSegment() { 92 | searchTypeSegment.rx.value 93 | .subscribe(onNext: { [weak self] index in 94 | switch index { 95 | case 0: 96 | UserSettings.setSearchType(SearchType.rank) 97 | self?.searchPeriodStackView.isHidden = false 98 | self?.searchPeriodTopMargin.priority = UILayoutPriority.defaultHigh 99 | case 1: 100 | UserSettings.setSearchType(SearchType.recent) 101 | self?.searchPeriodStackView.isHidden = true 102 | self?.searchPeriodTopMargin.priority = UILayoutPriority.defaultLow 103 | default: 104 | break 105 | } 106 | }) 107 | .addDisposableTo(bag) 108 | } 109 | 110 | func configureSearchPeriodSegment() { 111 | searchPeriodSegment.rx.value 112 | .subscribe(onNext: { index in 113 | switch index { 114 | case 0: 115 | UserSettings.setSearchPeriod(SearchPeriod.week) 116 | case 1: 117 | UserSettings.setSearchPeriod(SearchPeriod.month) 118 | default: 119 | break 120 | } 121 | }) 122 | .disposed(by: bag) 123 | } 124 | 125 | 126 | // MARK: - SearchHistoryCellDelegate 127 | 128 | func didTapDeleteBtn(on cell: UITableViewCell) { 129 | guard let indexPath = tableView.indexPath(for: cell) else { return } 130 | searchHistory.delete(index: indexPath.row) 131 | tableView.deleteRows(at: [indexPath], with: .automatic) 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /QiitaPocket/ViewModel/ArticleListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleListViewModel.swift 3 | // QiitaPocket 4 | // 5 | // Created by hirothings on 2016/12/18. 6 | // Copyright © 2016年 hiroshings. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class ArticleListViewModel { 15 | 16 | var hasData = Variable(false) 17 | var hasNextPage = Variable(false) 18 | let loadNextPageTrigger = PublishSubject() 19 | let alertTrigger = PublishSubject() 20 | var firstLoad: Observable<[Article]>! 21 | var additionalLoad: Observable<[Article]>! 22 | 23 | lazy var isLoading: SharedSequence = { 24 | return self.isLoadingVariable.asDriver() 25 | }() 26 | 27 | var bottomViewHeight: CGFloat { 28 | switch self.searchType { 29 | case.rank: 30 | return 0 31 | case .recent: 32 | return 60 33 | } 34 | } 35 | 36 | var searchTitle: String { 37 | var text: String = "" 38 | if searchType == .rank { 39 | switch searchPeriod { 40 | case .week: 41 | text = "週間" 42 | case .month: 43 | text = "月間" 44 | } 45 | text += "ランキング" 46 | } 47 | else { 48 | text = "新着順" 49 | } 50 | text += searchTag.isEmpty ? ": すべて" : ": \(searchTag)" 51 | return text 52 | } 53 | 54 | var titleColor: UIColor { 55 | if searchType == .recent { 56 | return UIColor.theme 57 | } 58 | switch searchPeriod { 59 | case .week: 60 | return UIColor.week 61 | case .month: 62 | return UIColor.month 63 | } 64 | } 65 | 66 | private let fetchRankingTrigger = PublishSubject<(tag: String, period: SearchPeriod)>() 67 | private var fetchRecentTrigger = PublishSubject<(tag: String, page: Int)>() 68 | private let loadCompleteTrigger = PublishSubject<[Article]>() 69 | private var isLoadingVariable = Variable(false) 70 | 71 | private let bag = DisposeBag() 72 | private var articles: [Article] = [] 73 | private var currentTag = "" 74 | private var currentPage: Int = 1 75 | 76 | private var searchType: SearchType { 77 | return UserSettings.getSearchType() 78 | } 79 | private var searchTag: String { 80 | return UserSettings.getcurrentTag() 81 | } 82 | private var searchPeriod: SearchPeriod { 83 | return UserSettings.getSearchPeriod() 84 | } 85 | 86 | 87 | init(fetchTrigger: PublishSubject) { 88 | 89 | self.configureRecentArticle() 90 | self.configureRanking() 91 | 92 | fetchTrigger.bind(onNext: { [weak self] (tag: String) in 93 | guard let `self` = self else { return } 94 | self.isLoadingVariable.value = true 95 | self.resetItems(tag: tag) 96 | self.updateSearchState(tag: tag) 97 | 98 | switch self.searchType { 99 | case .rank: 100 | self.fetchRankingTrigger.onNext((tag: tag, period: self.searchPeriod)) 101 | case .recent: 102 | self.fetchRecentTrigger.onNext((tag: tag, page: 1)) 103 | } 104 | }) 105 | .disposed(by: bag) 106 | 107 | firstLoad = loadCompleteTrigger 108 | .filter { _ in self.currentPage == 1 } 109 | .shareReplay(1) 110 | 111 | additionalLoad = loadCompleteTrigger 112 | .filter { _ in self.currentPage != 1 } 113 | .shareReplay(1) 114 | } 115 | 116 | 117 | // MARK: private method 118 | 119 | private func resetItems(tag: String) { 120 | self.currentTag = tag 121 | self.hasNextPage.value = false 122 | articles = [] 123 | currentPage = 1 124 | } 125 | 126 | private func configureRanking() { 127 | fetchRankingTrigger 128 | .do(onNext: { [unowned self] tuple in 129 | self.isLoadingVariable.value = true 130 | self.resetItems(tag: tuple.tag) 131 | }) 132 | .flatMap { 133 | Articles.fetchRankedPost(with: $0.tag, period: self.searchPeriod) 134 | } 135 | .observeOn(Dependencies.sharedInstance.mainScheduler) 136 | .subscribe( 137 | onNext: { [weak self] (model: Articles) in 138 | guard let `self` = self else { return } 139 | self.isLoadingVariable.value = false 140 | if model.items.isEmpty { 141 | self.hasData.value = false 142 | return 143 | } 144 | self.hasData.value = true 145 | let addedStateArticles = self.addReadLaterState(model.items) 146 | self.articles = addedStateArticles 147 | self.loadCompleteTrigger.onNext(self.articles) 148 | }, 149 | onError: { [weak self] (error: Error) in 150 | guard let `self` = self else { return } 151 | self.bindError(error) 152 | self.hasData.value = false 153 | self.isLoadingVariable.value = false 154 | self.configureRanking() // Disposeが破棄されるので、再度設定する TODO: 再起以外に方法はないのか? 155 | } 156 | ) 157 | .addDisposableTo(bag) 158 | } 159 | 160 | private func configureRecentArticle() { 161 | let nextPageRequest = loadNextPageTrigger 162 | .withLatestFrom(isLoading.asObservable()) 163 | .filter { !$0 && self.hasNextPage.value && self.searchType == .recent } 164 | .flatMap { [weak self] _ -> Observable<(tag: String, page: Int)> in 165 | guard let `self` = self else { return Observable.empty() } 166 | self.currentPage += 1 167 | return Observable.of((tag: self.currentTag, page: self.currentPage)) 168 | } 169 | .shareReplay(1) 170 | 171 | let request = Observable 172 | .of(fetchRecentTrigger, nextPageRequest) 173 | .merge() 174 | .shareReplay(1) 175 | 176 | request 177 | .do(onNext: { [unowned self] tuple in 178 | self.isLoadingVariable.value = true 179 | }) 180 | .flatMap { tuple in 181 | Articles.fetch(with: tuple.tag, page: tuple.page) 182 | } 183 | .observeOn(Dependencies.sharedInstance.mainScheduler) 184 | .subscribe( 185 | onNext: { [weak self] (model: Articles) in 186 | guard let `self` = self else { return } 187 | let _articles = model.items 188 | if _articles.isNotEmpty { 189 | self.hasData.value = true 190 | let addedStateArticles = self.addReadLaterState(_articles) 191 | self.articles += addedStateArticles 192 | self.loadCompleteTrigger.onNext(self.articles) 193 | } 194 | else { 195 | self.hasData.value = false 196 | } 197 | self.hasNextPage.value = (model.nextPage != nil) 198 | self.isLoadingVariable.value = false 199 | }, 200 | onError: { [weak self] (error) in 201 | guard let `self` = self else { return } 202 | self.bindError(error) 203 | self.hasData.value = false 204 | self.hasNextPage.value = false 205 | self.isLoadingVariable.value = false 206 | self.configureRecentArticle() 207 | } 208 | ).addDisposableTo(bag) 209 | } 210 | 211 | 212 | // MARK: - Private Method 213 | 214 | /// あとで読むステータスをarticleに付与する 215 | private func addReadLaterState(_ articles: [Article]) -> [Article] { 216 | let saveArtcleIDs: [String] = ArticleManager.getAll().map { $0.id } 217 | articles.forEach { (article: Article) in 218 | for id in saveArtcleIDs { 219 | if article.id == id { 220 | article.hasSaved = true 221 | break 222 | } 223 | } 224 | } 225 | return articles 226 | } 227 | 228 | private func bindError(_ error: Error) { 229 | switch error { 230 | case let error as QiitaAPIError: 231 | self.alertTrigger.onNext(error.message) 232 | case let error as ConnectionError: 233 | self.alertTrigger.onNext(error.message) 234 | default: 235 | self.alertTrigger.onNext(error.localizedDescription) 236 | } 237 | } 238 | 239 | private func updateSearchState(tag: String) { 240 | UserSettings.setcurrentTag(name: tag) 241 | let searchHistory = SearchHistory() 242 | searchHistory.add(tag: tag) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/Alamofire.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/) 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | 29 | Type 30 | PSGroupSpecifier 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/Firebase.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright 2017 Google 10 | Type 11 | PSGroupSpecifier 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/FirebaseAnalytics.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright 2017 Google 10 | Type 11 | PSGroupSpecifier 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/FirebaseCore.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright 2017 Google 10 | Type 11 | PSGroupSpecifier 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/FirebaseCrash.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright 2017 Google 10 | Type 11 | PSGroupSpecifier 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/FirebaseInstanceID.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright 2017 Google 10 | Type 11 | PSGroupSpecifier 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/Google-Mobile-Ads-SDK.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright 2017 Google 10 | Type 11 | PSGroupSpecifier 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/GoogleToolboxForMac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | 10 | Apache License 11 | Version 2.0, January 2004 12 | http://www.apache.org/licenses/ 13 | 14 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 15 | 16 | 1. Definitions. 17 | 18 | "License" shall mean the terms and conditions for use, reproduction, 19 | and distribution as defined by Sections 1 through 9 of this document. 20 | 21 | "Licensor" shall mean the copyright owner or entity authorized by 22 | the copyright owner that is granting the License. 23 | 24 | "Legal Entity" shall mean the union of the acting entity and all 25 | other entities that control, are controlled by, or are under common 26 | control with that entity. For the purposes of this definition, 27 | "control" means (i) the power, direct or indirect, to cause the 28 | direction or management of such entity, whether by contract or 29 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 30 | outstanding shares, or (iii) beneficial ownership of such entity. 31 | 32 | "You" (or "Your") shall mean an individual or Legal Entity 33 | exercising permissions granted by this License. 34 | 35 | "Source" form shall mean the preferred form for making modifications, 36 | including but not limited to software source code, documentation 37 | source, and configuration files. 38 | 39 | "Object" form shall mean any form resulting from mechanical 40 | transformation or translation of a Source form, including but 41 | not limited to compiled object code, generated documentation, 42 | and conversions to other media types. 43 | 44 | "Work" shall mean the work of authorship, whether in Source or 45 | Object form, made available under the License, as indicated by a 46 | copyright notice that is included in or attached to the work 47 | (an example is provided in the Appendix below). 48 | 49 | "Derivative Works" shall mean any work, whether in Source or Object 50 | form, that is based on (or derived from) the Work and for which the 51 | editorial revisions, annotations, elaborations, or other modifications 52 | represent, as a whole, an original work of authorship. For the purposes 53 | of this License, Derivative Works shall not include works that remain 54 | separable from, or merely link (or bind by name) to the interfaces of, 55 | the Work and Derivative Works thereof. 56 | 57 | "Contribution" shall mean any work of authorship, including 58 | the original version of the Work and any modifications or additions 59 | to that Work or Derivative Works thereof, that is intentionally 60 | submitted to Licensor for inclusion in the Work by the copyright owner 61 | or by an individual or Legal Entity authorized to submit on behalf of 62 | the copyright owner. For the purposes of this definition, "submitted" 63 | means any form of electronic, verbal, or written communication sent 64 | to the Licensor or its representatives, including but not limited to 65 | communication on electronic mailing lists, source code control systems, 66 | and issue tracking systems that are managed by, or on behalf of, the 67 | Licensor for the purpose of discussing and improving the Work, but 68 | excluding communication that is conspicuously marked or otherwise 69 | designated in writing by the copyright owner as "Not a Contribution." 70 | 71 | "Contributor" shall mean Licensor and any individual or Legal Entity 72 | on behalf of whom a Contribution has been received by Licensor and 73 | subsequently incorporated within the Work. 74 | 75 | 2. Grant of Copyright License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | copyright license to reproduce, prepare Derivative Works of, 79 | publicly display, publicly perform, sublicense, and distribute the 80 | Work and such Derivative Works in Source or Object form. 81 | 82 | 3. Grant of Patent License. Subject to the terms and conditions of 83 | this License, each Contributor hereby grants to You a perpetual, 84 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 85 | (except as stated in this section) patent license to make, have made, 86 | use, offer to sell, sell, import, and otherwise transfer the Work, 87 | where such license applies only to those patent claims licensable 88 | by such Contributor that are necessarily infringed by their 89 | Contribution(s) alone or by combination of their Contribution(s) 90 | with the Work to which such Contribution(s) was submitted. If You 91 | institute patent litigation against any entity (including a 92 | cross-claim or counterclaim in a lawsuit) alleging that the Work 93 | or a Contribution incorporated within the Work constitutes direct 94 | or contributory patent infringement, then any patent licenses 95 | granted to You under this License for that Work shall terminate 96 | as of the date such litigation is filed. 97 | 98 | 4. Redistribution. You may reproduce and distribute copies of the 99 | Work or Derivative Works thereof in any medium, with or without 100 | modifications, and in Source or Object form, provided that You 101 | meet the following conditions: 102 | 103 | (a) You must give any other recipients of the Work or 104 | Derivative Works a copy of this License; and 105 | 106 | (b) You must cause any modified files to carry prominent notices 107 | stating that You changed the files; and 108 | 109 | (c) You must retain, in the Source form of any Derivative Works 110 | that You distribute, all copyright, patent, trademark, and 111 | attribution notices from the Source form of the Work, 112 | excluding those notices that do not pertain to any part of 113 | the Derivative Works; and 114 | 115 | (d) If the Work includes a "NOTICE" text file as part of its 116 | distribution, then any Derivative Works that You distribute must 117 | include a readable copy of the attribution notices contained 118 | within such NOTICE file, excluding those notices that do not 119 | pertain to any part of the Derivative Works, in at least one 120 | of the following places: within a NOTICE text file distributed 121 | as part of the Derivative Works; within the Source form or 122 | documentation, if provided along with the Derivative Works; or, 123 | within a display generated by the Derivative Works, if and 124 | wherever such third-party notices normally appear. The contents 125 | of the NOTICE file are for informational purposes only and 126 | do not modify the License. You may add Your own attribution 127 | notices within Derivative Works that You distribute, alongside 128 | or as an addendum to the NOTICE text from the Work, provided 129 | that such additional attribution notices cannot be construed 130 | as modifying the License. 131 | 132 | You may add Your own copyright statement to Your modifications and 133 | may provide additional or different license terms and conditions 134 | for use, reproduction, or distribution of Your modifications, or 135 | for any such Derivative Works as a whole, provided Your use, 136 | reproduction, and distribution of the Work otherwise complies with 137 | the conditions stated in this License. 138 | 139 | 5. Submission of Contributions. Unless You explicitly state otherwise, 140 | any Contribution intentionally submitted for inclusion in the Work 141 | by You to the Licensor shall be under the terms and conditions of 142 | this License, without any additional terms or conditions. 143 | Notwithstanding the above, nothing herein shall supersede or modify 144 | the terms of any separate license agreement you may have executed 145 | with Licensor regarding such Contributions. 146 | 147 | 6. Trademarks. This License does not grant permission to use the trade 148 | names, trademarks, service marks, or product names of the Licensor, 149 | except as required for reasonable and customary use in describing the 150 | origin of the Work and reproducing the content of the NOTICE file. 151 | 152 | 7. Disclaimer of Warranty. Unless required by applicable law or 153 | agreed to in writing, Licensor provides the Work (and each 154 | Contributor provides its Contributions) on an "AS IS" BASIS, 155 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 156 | implied, including, without limitation, any warranties or conditions 157 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 158 | PARTICULAR PURPOSE. You are solely responsible for determining the 159 | appropriateness of using or redistributing the Work and assume any 160 | risks associated with Your exercise of permissions under this License. 161 | 162 | 8. Limitation of Liability. In no event and under no legal theory, 163 | whether in tort (including negligence), contract, or otherwise, 164 | unless required by applicable law (such as deliberate and grossly 165 | negligent acts) or agreed to in writing, shall any Contributor be 166 | liable to You for damages, including any direct, indirect, special, 167 | incidental, or consequential damages of any character arising as a 168 | result of this License or out of the use or inability to use the 169 | Work (including but not limited to damages for loss of goodwill, 170 | work stoppage, computer failure or malfunction, or any and all 171 | other commercial damages or losses), even if such Contributor 172 | has been advised of the possibility of such damages. 173 | 174 | 9. Accepting Warranty or Additional Liability. While redistributing 175 | the Work or Derivative Works thereof, You may choose to offer, 176 | and charge a fee for, acceptance of support, warranty, indemnity, 177 | or other liability obligations and/or rights consistent with this 178 | License. However, in accepting such obligations, You may act only 179 | on Your own behalf and on Your sole responsibility, not on behalf 180 | of any other Contributor, and only if You agree to indemnify, 181 | defend, and hold each Contributor harmless for any liability 182 | incurred by, or claims asserted against, such Contributor by reason 183 | of your accepting any such warranty or additional liability. 184 | 185 | END OF TERMS AND CONDITIONS 186 | 187 | APPENDIX: How to apply the Apache License to your work. 188 | 189 | To apply the Apache License to your work, attach the following 190 | boilerplate notice, with the fields enclosed by brackets "[]" 191 | replaced with your own identifying information. (Don't include 192 | the brackets!) The text should be enclosed in the appropriate 193 | comment syntax for the file format. We also recommend that a 194 | file or class name and description of purpose be included on the 195 | same "printed page" as the copyright notice for easier 196 | identification within third-party archives. 197 | 198 | Copyright [yyyy] [name of copyright owner] 199 | 200 | Licensed under the Apache License, Version 2.0 (the "License"); 201 | you may not use this file except in compliance with the License. 202 | You may obtain a copy of the License at 203 | 204 | http://www.apache.org/licenses/LICENSE-2.0 205 | 206 | Unless required by applicable law or agreed to in writing, software 207 | distributed under the License is distributed on an "AS IS" BASIS, 208 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 209 | See the License for the specific language governing permissions and 210 | limitations under the License. 211 | 212 | Type 213 | PSGroupSpecifier 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/Protobuf.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This license applies to all parts of Protocol Buffers except the following: 10 | 11 | - Atomicops support for generic gcc, located in 12 | src/google/protobuf/stubs/atomicops_internals_generic_gcc.h. 13 | This file is copyrighted by Red Hat Inc. 14 | 15 | - Atomicops support for AIX/POWER, located in 16 | src/google/protobuf/stubs/atomicops_internals_power.h. 17 | This file is copyrighted by Bloomberg Finance LP. 18 | 19 | Copyright 2014, Google Inc. All rights reserved. 20 | 21 | Redistribution and use in source and binary forms, with or without 22 | modification, are permitted provided that the following conditions are 23 | met: 24 | 25 | * Redistributions of source code must retain the above copyright 26 | notice, this list of conditions and the following disclaimer. 27 | * Redistributions in binary form must reproduce the above 28 | copyright notice, this list of conditions and the following disclaimer 29 | in the documentation and/or other materials provided with the 30 | distribution. 31 | * Neither the name of Google Inc. nor the names of its 32 | contributors may be used to endorse or promote products derived from 33 | this software without specific prior written permission. 34 | 35 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 36 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 37 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 38 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 39 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 40 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 41 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 42 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 43 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 44 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 45 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 46 | 47 | Code generated by the Protocol Buffer compiler is owned by the owner 48 | of the input file used when generating it. This code is not 49 | standalone and requires a support library to be linked with it. This 50 | support library is itself covered by the above license. 51 | 52 | Type 53 | PSGroupSpecifier 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/RxDataSources.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | MIT License 10 | 11 | Copyright (c) 2017 RxSwift Community 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | 31 | Type 32 | PSGroupSpecifier 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/RxSwift.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | **The MIT License** 10 | **Copyright © 2015 Krunoslav Zaher** 11 | **All rights reserved.** 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | Type 19 | PSGroupSpecifier 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/SDWebImage.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright (c) 2009-2017 Olivier Poitrey rs@dailymotion.com 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is furnished 16 | to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | 29 | 30 | Type 31 | PSGroupSpecifier 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/SwiftyJSON.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2017 Ruoyu Fu 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | THE SOFTWARE. 30 | 31 | Type 32 | PSGroupSpecifier 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/XLPagerTabStrip.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2017 Xmartlabs SRL 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | 31 | Type 32 | PSGroupSpecifier 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/com.mono0926.LicensePlist.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | File 9 | com.mono0926.LicensePlist/Alamofire 10 | Title 11 | Alamofire 12 | Type 13 | PSChildPaneSpecifier 14 | 15 | 16 | File 17 | com.mono0926.LicensePlist/Firebase 18 | Title 19 | Firebase 20 | Type 21 | PSChildPaneSpecifier 22 | 23 | 24 | File 25 | com.mono0926.LicensePlist/FirebaseAnalytics 26 | Title 27 | FirebaseAnalytics 28 | Type 29 | PSChildPaneSpecifier 30 | 31 | 32 | File 33 | com.mono0926.LicensePlist/FirebaseCore 34 | Title 35 | FirebaseCore 36 | Type 37 | PSChildPaneSpecifier 38 | 39 | 40 | File 41 | com.mono0926.LicensePlist/FirebaseCrash 42 | Title 43 | FirebaseCrash 44 | Type 45 | PSChildPaneSpecifier 46 | 47 | 48 | File 49 | com.mono0926.LicensePlist/FirebaseInstanceID 50 | Title 51 | FirebaseInstanceID 52 | Type 53 | PSChildPaneSpecifier 54 | 55 | 56 | File 57 | com.mono0926.LicensePlist/Google-Mobile-Ads-SDK 58 | Title 59 | Google-Mobile-Ads-SDK 60 | Type 61 | PSChildPaneSpecifier 62 | 63 | 64 | File 65 | com.mono0926.LicensePlist/GoogleToolboxForMac 66 | Title 67 | GoogleToolboxForMac 68 | Type 69 | PSChildPaneSpecifier 70 | 71 | 72 | File 73 | com.mono0926.LicensePlist/Protobuf 74 | Title 75 | Protobuf 76 | Type 77 | PSChildPaneSpecifier 78 | 79 | 80 | File 81 | com.mono0926.LicensePlist/realm-cocoa 82 | Title 83 | realm-cocoa 84 | Type 85 | PSChildPaneSpecifier 86 | 87 | 88 | File 89 | com.mono0926.LicensePlist/RxDataSources 90 | Title 91 | RxDataSources 92 | Type 93 | PSChildPaneSpecifier 94 | 95 | 96 | File 97 | com.mono0926.LicensePlist/RxSwift 98 | Title 99 | RxSwift 100 | Type 101 | PSChildPaneSpecifier 102 | 103 | 104 | File 105 | com.mono0926.LicensePlist/SDWebImage 106 | Title 107 | SDWebImage 108 | Type 109 | PSChildPaneSpecifier 110 | 111 | 112 | File 113 | com.mono0926.LicensePlist/SwiftyJSON 114 | Title 115 | SwiftyJSON 116 | Type 117 | PSChildPaneSpecifier 118 | 119 | 120 | File 121 | com.mono0926.LicensePlist/XLPagerTabStrip 122 | Title 123 | XLPagerTabStrip 124 | Type 125 | PSChildPaneSpecifier 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /QiitaPocket/com.mono0926.LicensePlist/realm-cocoa.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | TABLE OF CONTENTS 10 | 11 | 1. Apache License version 2.0 12 | 2. Realm Components 13 | 3. Export Compliance 14 | 15 | 1. ------------------------------------------------------------------------------- 16 | 17 | Apache License 18 | Version 2.0, January 2004 19 | http://www.apache.org/licenses/ 20 | 21 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 22 | 23 | 1. Definitions. 24 | 25 | "License" shall mean the terms and conditions for use, reproduction, 26 | and distribution as defined by Sections 1 through 9 of this document. 27 | 28 | "Licensor" shall mean the copyright owner or entity authorized by 29 | the copyright owner that is granting the License. 30 | 31 | "Legal Entity" shall mean the union of the acting entity and all 32 | other entities that control, are controlled by, or are under common 33 | control with that entity. For the purposes of this definition, 34 | "control" means (i) the power, direct or indirect, to cause the 35 | direction or management of such entity, whether by contract or 36 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 37 | outstanding shares, or (iii) beneficial ownership of such entity. 38 | 39 | "You" (or "Your") shall mean an individual or Legal Entity 40 | exercising permissions granted by this License. 41 | 42 | "Source" form shall mean the preferred form for making modifications, 43 | including but not limited to software source code, documentation 44 | source, and configuration files. 45 | 46 | "Object" form shall mean any form resulting from mechanical 47 | transformation or translation of a Source form, including but 48 | not limited to compiled object code, generated documentation, 49 | and conversions to other media types. 50 | 51 | "Work" shall mean the work of authorship, whether in Source or 52 | Object form, made available under the License, as indicated by a 53 | copyright notice that is included in or attached to the work 54 | (an example is provided in the Appendix below). 55 | 56 | "Derivative Works" shall mean any work, whether in Source or Object 57 | form, that is based on (or derived from) the Work and for which the 58 | editorial revisions, annotations, elaborations, or other modifications 59 | represent, as a whole, an original work of authorship. For the purposes 60 | of this License, Derivative Works shall not include works that remain 61 | separable from, or merely link (or bind by name) to the interfaces of, 62 | the Work and Derivative Works thereof. 63 | 64 | "Contribution" shall mean any work of authorship, including 65 | the original version of the Work and any modifications or additions 66 | to that Work or Derivative Works thereof, that is intentionally 67 | submitted to Licensor for inclusion in the Work by the copyright owner 68 | or by an individual or Legal Entity authorized to submit on behalf of 69 | the copyright owner. For the purposes of this definition, "submitted" 70 | means any form of electronic, verbal, or written communication sent 71 | to the Licensor or its representatives, including but not limited to 72 | communication on electronic mailing lists, source code control systems, 73 | and issue tracking systems that are managed by, or on behalf of, the 74 | Licensor for the purpose of discussing and improving the Work, but 75 | excluding communication that is conspicuously marked or otherwise 76 | designated in writing by the copyright owner as "Not a Contribution." 77 | 78 | "Contributor" shall mean Licensor and any individual or Legal Entity 79 | on behalf of whom a Contribution has been received by Licensor and 80 | subsequently incorporated within the Work. 81 | 82 | 2. Grant of Copyright License. Subject to the terms and conditions of 83 | this License, each Contributor hereby grants to You a perpetual, 84 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 85 | copyright license to reproduce, prepare Derivative Works of, 86 | publicly display, publicly perform, sublicense, and distribute the 87 | Work and such Derivative Works in Source or Object form. 88 | 89 | 3. Grant of Patent License. Subject to the terms and conditions of 90 | this License, each Contributor hereby grants to You a perpetual, 91 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 92 | (except as stated in this section) patent license to make, have made, 93 | use, offer to sell, sell, import, and otherwise transfer the Work, 94 | where such license applies only to those patent claims licensable 95 | by such Contributor that are necessarily infringed by their 96 | Contribution(s) alone or by combination of their Contribution(s) 97 | with the Work to which such Contribution(s) was submitted. If You 98 | institute patent litigation against any entity (including a 99 | cross-claim or counterclaim in a lawsuit) alleging that the Work 100 | or a Contribution incorporated within the Work constitutes direct 101 | or contributory patent infringement, then any patent licenses 102 | granted to You under this License for that Work shall terminate 103 | as of the date such litigation is filed. 104 | 105 | 4. Redistribution. You may reproduce and distribute copies of the 106 | Work or Derivative Works thereof in any medium, with or without 107 | modifications, and in Source or Object form, provided that You 108 | meet the following conditions: 109 | 110 | (a) You must give any other recipients of the Work or 111 | Derivative Works a copy of this License; and 112 | 113 | (b) You must cause any modified files to carry prominent notices 114 | stating that You changed the files; and 115 | 116 | (c) You must retain, in the Source form of any Derivative Works 117 | that You distribute, all copyright, patent, trademark, and 118 | attribution notices from the Source form of the Work, 119 | excluding those notices that do not pertain to any part of 120 | the Derivative Works; and 121 | 122 | (d) If the Work includes a "NOTICE" text file as part of its 123 | distribution, then any Derivative Works that You distribute must 124 | include a readable copy of the attribution notices contained 125 | within such NOTICE file, excluding those notices that do not 126 | pertain to any part of the Derivative Works, in at least one 127 | of the following places: within a NOTICE text file distributed 128 | as part of the Derivative Works; within the Source form or 129 | documentation, if provided along with the Derivative Works; or, 130 | within a display generated by the Derivative Works, if and 131 | wherever such third-party notices normally appear. The contents 132 | of the NOTICE file are for informational purposes only and 133 | do not modify the License. You may add Your own attribution 134 | notices within Derivative Works that You distribute, alongside 135 | or as an addendum to the NOTICE text from the Work, provided 136 | that such additional attribution notices cannot be construed 137 | as modifying the License. 138 | 139 | You may add Your own copyright statement to Your modifications and 140 | may provide additional or different license terms and conditions 141 | for use, reproduction, or distribution of Your modifications, or 142 | for any such Derivative Works as a whole, provided Your use, 143 | reproduction, and distribution of the Work otherwise complies with 144 | the conditions stated in this License. 145 | 146 | 5. Submission of Contributions. Unless You explicitly state otherwise, 147 | any Contribution intentionally submitted for inclusion in the Work 148 | by You to the Licensor shall be under the terms and conditions of 149 | this License, without any additional terms or conditions. 150 | Notwithstanding the above, nothing herein shall supersede or modify 151 | the terms of any separate license agreement you may have executed 152 | with Licensor regarding such Contributions. 153 | 154 | 6. Trademarks. This License does not grant permission to use the trade 155 | names, trademarks, service marks, or product names of the Licensor, 156 | except as required for reasonable and customary use in describing the 157 | origin of the Work and reproducing the content of the NOTICE file. 158 | 159 | 7. Disclaimer of Warranty. Unless required by applicable law or 160 | agreed to in writing, Licensor provides the Work (and each 161 | Contributor provides its Contributions) on an "AS IS" BASIS, 162 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 163 | implied, including, without limitation, any warranties or conditions 164 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 165 | PARTICULAR PURPOSE. You are solely responsible for determining the 166 | appropriateness of using or redistributing the Work and assume any 167 | risks associated with Your exercise of permissions under this License. 168 | 169 | 8. Limitation of Liability. In no event and under no legal theory, 170 | whether in tort (including negligence), contract, or otherwise, 171 | unless required by applicable law (such as deliberate and grossly 172 | negligent acts) or agreed to in writing, shall any Contributor be 173 | liable to You for damages, including any direct, indirect, special, 174 | incidental, or consequential damages of any character arising as a 175 | result of this License or out of the use or inability to use the 176 | Work (including but not limited to damages for loss of goodwill, 177 | work stoppage, computer failure or malfunction, or any and all 178 | other commercial damages or losses), even if such Contributor 179 | has been advised of the possibility of such damages. 180 | 181 | 9. Accepting Warranty or Additional Liability. While redistributing 182 | the Work or Derivative Works thereof, You may choose to offer, 183 | and charge a fee for, acceptance of support, warranty, indemnity, 184 | or other liability obligations and/or rights consistent with this 185 | License. However, in accepting such obligations, You may act only 186 | on Your own behalf and on Your sole responsibility, not on behalf 187 | of any other Contributor, and only if You agree to indemnify, 188 | defend, and hold each Contributor harmless for any liability 189 | incurred by, or claims asserted against, such Contributor by reason 190 | of your accepting any such warranty or additional liability. 191 | 192 | 2. ------------------------------------------------------------------------------- 193 | 194 | REALM COMPONENTS 195 | 196 | This software contains components with separate copyright and license terms. 197 | Your use of these components is subject to the terms and conditions of the 198 | following licenses. 199 | 200 | For the Realm Platform Extensions component 201 | 202 | Realm Platform Extensions License 203 | 204 | Copyright (c) 2011-2017 Realm Inc All rights reserved 205 | 206 | Redistribution and use in binary form, with or without modification, is 207 | permitted provided that the following conditions are met: 208 | 209 | 1. You agree not to attempt to decompile, disassemble, reverse engineer or 210 | otherwise discover the source code from which the binary code was derived. 211 | You may, however, access and obtain a separate license for most of the 212 | source code from which this Software was created, at 213 | http://realm.io/pricing/. 214 | 215 | 2. Redistributions in binary form must reproduce the above copyright notice, 216 | this list of conditions and the following disclaimer in the documentation 217 | and/or other materials provided with the distribution. 218 | 219 | 3. Neither the name of the copyright holder nor the names of its 220 | contributors may be used to endorse or promote products derived from this 221 | software without specific prior written permission. 222 | 223 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 224 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 225 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 226 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 227 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 228 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 229 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 230 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 231 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 232 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 233 | POSSIBILITY OF SUCH DAMAGE. 234 | 235 | 3. ------------------------------------------------------------------------------- 236 | 237 | EXPORT COMPLIANCE 238 | 239 | You understand that the Software may contain cryptographic functions that may be 240 | subject to export restrictions, and you represent and warrant that you are not 241 | (i) located in a jurisdiction that is subject to United States economic 242 | sanctions (“Prohibited Jurisdiction”), including Cuba, Iran, North Korea, 243 | Sudan, Syria or the Crimea region, (ii) a person listed on any U.S. government 244 | blacklist (to include the List of Specially Designated Nationals and Blocked 245 | Persons or the Consolidated Sanctions List administered by the U.S. Department 246 | of the Treasury’s Office of Foreign Assets Control, or the Denied Persons List 247 | or Entity List administered by the U.S. Department of Commerce) 248 | (“Sanctioned Person”), or (iii) controlled or 50% or more owned by a Sanctioned 249 | Person. 250 | 251 | You agree to comply with all export, re-export and import restrictions and 252 | regulations of the U.S. Department of Commerce or other agency or authority of 253 | the United States or other applicable countries. You also agree not to transfer, 254 | or authorize the transfer of, directly or indirectly, of the Software to any 255 | Prohibited Jurisdiction, or otherwise in violation of any such restrictions or 256 | regulations. 257 | 258 | Type 259 | PSGroupSpecifier 260 | 261 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qiita Pocket 2 | あとで読むQiita APP (Swift4.x / Xcode9) 3 | 4 | ## どんなアプリ? 5 | 6 | ![](https://media.giphy.com/media/xUPGcryOw0wYY0PaJa/giphy.gif) 7 | 8 | Qiitaの気になる記事をローカルに保存しスキマ時間に閲覧できる **"あとで読む"** Qiitaリーダーアプリです。 9 | 10 | **APP Store** 11 | https://appsto.re/jp/yLaTib.i 12 | 13 | ## 制作秘話 14 | 15 | [あとで読むQiitaリーダーアプリをリリースしました](http://qiita.com/hirothings/items/78493363df04e5f31d25) 16 | 17 | ## Feature 18 | 19 | - [x] 新着記事の追加ローディング 20 | - [ ] タグ一覧の表示 21 | - [x] 月間ランキング 22 | --------------------------------------------------------------------------------