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