├── RubyChinaApp
├── Assets.xcassets
│ ├── Contents.json
│ ├── first.imageset
│ │ ├── first.pdf
│ │ └── Contents.json
│ ├── second.imageset
│ │ ├── second.pdf
│ │ └── Contents.json
│ ├── avatarPlaceHolder.imageset
│ │ ├── avatar.png
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Libraries
│ ├── ViewModelType.swift
│ ├── UIEmbedsInCellButton.swift
│ ├── Endpoints
│ │ ├── RubyChinaV3.swift
│ │ ├── Hello.swift
│ │ ├── Nodes.swift
│ │ ├── Photos.swift
│ │ ├── Replies.swift
│ │ ├── Topics.swift
│ │ └── Users.swift
│ ├── APIResult.swift
│ ├── Extensions
│ │ ├── Response+mapSwiftyJSON.swift
│ │ ├── String.swift
│ │ └── CustomDebugStringConvertible.swift
│ ├── ModelType.swift
│ ├── SwiftyJSONMappable.swift
│ ├── NetworkLogger.swift
│ ├── Pagers
│ │ └── TopicsPager.swift
│ ├── SegueHandlerType.swift
│ ├── APIError.swift
│ ├── Helpers.swift
│ ├── EndpointType.swift
│ ├── OffsetPager.swift
│ └── Provider.swift
├── GlobalConstant.swift
├── Models
│ ├── Ability.swift
│ ├── Node.swift
│ ├── Reply.swift
│ ├── Topic.swift
│ └── User.swift
├── SharedObjects.swift
├── Controllers
│ ├── Topics
│ │ ├── TopicsTableViewCell.swift
│ │ ├── TopicCellViewModel.swift
│ │ └── TopicsViewController.swift
│ ├── UserDetail
│ │ └── UserDetailViewController.swift
│ ├── TopicDetail
│ │ └── TopicDetailViewController.swift
│ └── FirstViewController.swift
├── Info.plist
├── AppDelegate.swift
└── Views
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── RubyChinaApp.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── project.pbxproj
├── README.md
├── RubyChinaApp.xcworkspace
└── contents.xcworkspacedata
├── Podfile
├── RubyChinaAppTests
├── Info.plist
└── RubyChinaAppTests.swift
├── RubyChinaAppUITests
├── Info.plist
└── RubyChinaAppUITests.swift
├── .gitignore
└── Podfile.lock
/RubyChinaApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/RubyChinaApp/Assets.xcassets/first.imageset/first.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jasl/RubyChinaAPP/HEAD/RubyChinaApp/Assets.xcassets/first.imageset/first.pdf
--------------------------------------------------------------------------------
/RubyChinaApp/Assets.xcassets/second.imageset/second.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jasl/RubyChinaAPP/HEAD/RubyChinaApp/Assets.xcassets/second.imageset/second.pdf
--------------------------------------------------------------------------------
/RubyChinaApp/Assets.xcassets/avatarPlaceHolder.imageset/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jasl/RubyChinaAPP/HEAD/RubyChinaApp/Assets.xcassets/avatarPlaceHolder.imageset/avatar.png
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/ViewModelType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 1/7/16.
3 | // Copyright (c) 2016 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | protocol ViewModelType {
9 | }
10 |
--------------------------------------------------------------------------------
/RubyChinaApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RubyChinaApp/Assets.xcassets/first.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "first.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/UIEmbedsInCellButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 2/10/16.
3 | // Copyright (c) 2016 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import UIKit
8 |
9 | class EmbedsInTableViewCellUIButton: UIButton {}
10 |
--------------------------------------------------------------------------------
/RubyChinaApp/Assets.xcassets/second.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "second.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Endpoints/RubyChinaV3.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/13/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | struct RubyChinaV3 {
9 | static let BaseURL = NSURL(string: "https://ruby-china.org/api/v3/")!
10 | }
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Ruby China App by jasl
2 | =====
3 |
4 | 这是我用来学习 iOS 开发的项目, 目标并不是产出一款易用的 APP
5 |
6 | 我的目标:
7 | - 掌握 Swift 语言
8 | - 理解 iOS 开发
9 | - 考察、试用各种用途的库
10 | - 通过实现来证明设想中的 APP结构
11 |
12 | 由于这是我的第一个 iOS 项目, 也尚未系统学习过, 所以想法变化频繁, 提交会比较混乱,
13 | 会有很多幼稚之处, 如果你有草览过代码, 希望你可以为我指出问题、分享经验!
14 |
15 | MIT Licence.
16 |
17 |
--------------------------------------------------------------------------------
/RubyChinaApp.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/APIResult.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/17/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | enum APIResult {
11 | case Ok
12 | case Success(T)
13 | case Failure(APIError)
14 | case NetworkError(MoyaX.Error)
15 | }
16 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Extensions/Response+mapSwiftyJSON.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/14/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | extension Response {
11 | func mapSwiftyJSON() -> JSON {
12 | return JSON(data: data)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Extensions/String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/12/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | extension String {
9 | var URLEscapedString: String {
10 | return self.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLHostAllowedCharacterSet())!
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/RubyChinaApp/Assets.xcassets/avatarPlaceHolder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "avatar.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RubyChinaApp/GlobalConstant.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 7/13/15.
3 | // Copyright (c) 2015 KnewOne. All rights reserved.
4 | //
5 |
6 | import XCGLogger
7 |
8 | struct GlobalConstant {
9 | static let clientId = "1b034acf"
10 | static let clientSecret = "2d44bae75daaa88f2b8226a0205318b6ccf79b09e80fbfb461d191001d7b3c7b"
11 | static let redirectURIs = ["rbcn://oauth/callback"]
12 |
13 | static let logLevel: XCGLogger.LogLevel = .Verbose
14 | }
15 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/ModelType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/31/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | protocol ModelType: SwiftyJSONMappable, Equatable, Hashable {
9 | var identifier: String { get }
10 | }
11 |
12 | extension ModelType {
13 | var hashValue: Int {
14 | return self.identifier.hashValue
15 | }
16 | }
17 |
18 | func ==(lhs: T, rhs: T) -> Bool {
19 | return lhs.hashValue == rhs.hashValue
20 | }
21 |
--------------------------------------------------------------------------------
/RubyChinaApp/Models/Ability.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 7/10/15.
3 | // Copyright (c) 2015 KnewOne. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 |
9 | struct Ability: CustomDebugStringConvertible, SwiftyJSONMappable {
10 | let canUpdate: Bool
11 | let canDestroy: Bool
12 |
13 | init?(byJSON json: JSON) {
14 | if json.type == .Null { return nil }
15 |
16 | self.canUpdate = json["update"].boolValue
17 | self.canDestroy = json["destroy"].boolValue
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Endpoints/Hello.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/16/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | extension RubyChinaV3 {
11 | struct Hello: EndpointType {
12 | let baseURL = RubyChinaV3.BaseURL
13 | let path = "hello"
14 |
15 | typealias DeserializedType = User
16 |
17 | func parseResponse(json: JSON) -> DeserializedType? {
18 | return DeserializedType(byJSON: json["user"])
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '9.0'
2 | use_frameworks!
3 |
4 | target 'RubyChinaApp' do
5 | # arch
6 | # pod 'RxSwift'
7 |
8 | # networking
9 | pod 'Alamofire'
10 | pod 'MoyaX'
11 | # pod 'ReachabilitySwift', git: 'https://github.com/ashleymills/Reachability.swift'
12 |
13 | # serializing
14 | pod 'SwiftyJSON'
15 |
16 | # logging
17 | pod 'XCGLogger'
18 |
19 | # OAuth
20 | pod 'p2.OAuth2'
21 |
22 | # caching
23 | # pod 'HanekeSwift'
24 |
25 | # util
26 | pod 'SwiftDate'
27 |
28 | # UI
29 | pod 'MJRefresh'
30 | # pod 'DGTemplateLayoutCell'
31 | pod 'Kingfisher'
32 | end
33 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/SwiftyJSONMappable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 7/13/15.
3 | // Copyright (c) 2015 KnewOne. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 |
9 | protocol SwiftyJSONMappable {
10 | init?(byJSON json: JSON)
11 | }
12 |
13 | extension Array where Element: SwiftyJSONMappable {
14 | init(byJSON json: JSON) {
15 | self.init()
16 |
17 | if json.type == .Null { return }
18 |
19 | for item in json.arrayValue {
20 | if let object = Element.init(byJSON: item) {
21 | self.append(object)
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/RubyChinaApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/RubyChinaApp/SharedObjects.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/17/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import XCGLogger
8 |
9 | extension Provider {
10 | class func defaultInstance() -> Provider {
11 | struct statics {
12 | static let instance: Provider = Provider(clientID: GlobalConstant.clientId,
13 | clientSecret: GlobalConstant.clientSecret,
14 | redirect_uris: GlobalConstant.redirectURIs,
15 | middlewares: [NetworkLogger()])
16 | }
17 |
18 | return statics.instance
19 | }
20 | }
21 |
22 | let logger = XCGLogger.defaultInstance()
23 | let provider = Provider.defaultInstance()
24 |
--------------------------------------------------------------------------------
/RubyChinaAppTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en_US
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/RubyChinaAppUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/NetworkLogger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/17/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import MoyaX
8 |
9 | /// Logs network activity (outgoing requests and incoming responses).
10 | class NetworkLogger: Middleware {
11 |
12 | func willSendRequest(target: Target, endpoint: Endpoint) {
13 | logger.info("Sending request: \(endpoint.URL.absoluteString)")
14 | }
15 |
16 | func didReceiveResponse(target: Target, response: Result) {
17 | switch response {
18 | case let .Response(response):
19 | logger.info("Received response(\(response.statusCode ?? 0)) from \(response.response!.URL?.absoluteString ?? String()).")
20 | case .Incomplete(_):
21 | logger.error("Got error")
22 | }
23 |
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Pagers/TopicsPager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/28/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | // TopicsPager 是对 RubyChinaV3.Topics.Listing 的封装,
9 | // 这种返回集合的 API 通常用于 UITableView 等列表页面, 通过 Pager 类可以很好的屏蔽掉加载新旧数据的逻辑,
10 | // 其仅暴露 UI关心的加载新数据(下拉刷新)和加载更早的数据(下拉加载更多), 如游标或指针的维持,
11 | // 同样的, Pager 返回的形式与正常途径调用的 Endpoint 的返回值及回调函数签名一致
12 | // 需要注意的是, 虽然 RubyChina 的 API 并没有使用游标方式实现, 但原理相同, 若采取游标方式实现分页, 可简化 Pager 的实现
13 | // 这里必须是基于类实现, 因为要在 Endpoint 的异步回调中修改自身状态
14 | class TopicsPager: OffsetPager {
15 | init(withPage page: Int = 1, withType type: RubyChinaV3.Topics.Listing.TypeFieldValue? = nil, withNodeId nodeId: String? = nil, withPerPage perPage: Int = 20) {
16 | super.init(endpoint: RubyChinaV3.Topics.Listing(type: type, nodeId: nodeId), withPerPage: perPage)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/RubyChinaApp/Controllers/Topics/TopicsTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicsTableViewCell.swift
3 | // RubyChinaApp
4 | //
5 | // Created by 姜军 on 12/23/15.
6 | // Copyright © 2015 RubyChina. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicsTableViewCell: UITableViewCell {
12 |
13 | // MARK: Properties
14 | @IBOutlet weak var titleLabel: UILabel!
15 | @IBOutlet weak var repliesCountLabel: UILabel!
16 | @IBOutlet weak var nodeNameButton: EmbedsInTableViewCellUIButton!
17 | @IBOutlet weak var authorNameButton: EmbedsInTableViewCellUIButton!
18 | @IBOutlet weak var authorAvatarImageButton: EmbedsInTableViewCellUIButton!
19 |
20 | override func awakeFromNib() {
21 | super.awakeFromNib()
22 | // Initialization code
23 | }
24 |
25 | override func prepareForReuse() {
26 | self.authorAvatarImageButton.kf_cancelImageDownloadTask()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/SegueHandlerType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 2/15/16.
3 | // Copyright (c) 2016 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import UIKit
8 |
9 | // From WWDC2015-414
10 | protocol SegueHandlerType {
11 | associatedtype SegueIdentifier: RawRepresentable
12 | }
13 |
14 | extension SegueHandlerType where Self: UIViewController, SegueIdentifier.RawValue == String {
15 | func performSegueWithIdentifier(segueIdentifier: SegueIdentifier, sender: AnyObject?) {
16 | performSegueWithIdentifier(segueIdentifier.rawValue, sender: sender)
17 | }
18 |
19 | func segueIdentifierForSegue(segue: UIStoryboardSegue) -> SegueIdentifier {
20 | guard let identifier = segue.identifier,
21 | segueIdentifier = SegueIdentifier(rawValue: identifier)
22 | else {
23 | fatalError("Invalid segue identifier \(segue.identifier)")
24 | }
25 |
26 | return segueIdentifier
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/APIError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/17/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 |
9 | struct APIError: CustomDebugStringConvertible, ErrorType {
10 | let message: String
11 | let type: ErrorCodeType
12 |
13 | init(statusCode: Int, message: String) {
14 | self.type = ErrorCodeType(byStatusCode: statusCode)
15 | self.message = message
16 | }
17 | }
18 |
19 | extension APIError {
20 | enum ErrorCodeType: Int {
21 | case BadRequest = 400
22 | case UnAuthorized = 401
23 | case Forbidden = 403
24 | case NotFound = 404
25 | case Unknown = 999
26 |
27 | init(byStatusCode statusCode: Int) {
28 | if let knownError = self.dynamicType.init(rawValue: statusCode) {
29 | self = knownError
30 | } else {
31 | self = .Unknown
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.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 | *.xccheckout
22 | *.moved-aside
23 | *.xcuserstate
24 | *.xcscmblueprint
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 | *.ipa
29 |
30 | # CocoaPods
31 | #
32 | # We recommend against adding the Pods directory to your .gitignore. However
33 | # you should judge for yourself, the pros and cons are mentioned at:
34 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
35 | #
36 | Pods/
37 |
38 | # Carthage
39 | #
40 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
41 | # Carthage/Checkouts
42 |
43 | Carthage/Build
44 |
45 | # AppCode
46 | .idea
47 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 7/14/15.
3 | // Copyright (c) 2015 KnewOne. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import UIKit
8 |
9 | func selectedRowAtIndexPathFor(tableView: UITableView, sender: AnyObject?) -> Int? {
10 | guard let sender = sender as? UIView else { return nil }
11 |
12 | switch sender {
13 | case let sender as UITableViewCell:
14 | return tableView.indexPathForCell(sender)?.row
15 | case let sender as EmbedsInTableViewCellUIButton:
16 | guard let cell = recurisiveSearchParentViewUntil(UITableViewCell.self, currentView: sender) else { return nil }
17 | return tableView.indexPathForCell(cell)?.row
18 | default:
19 | return nil
20 | }
21 | }
22 |
23 | func recurisiveSearchParentViewUntil(type: T.Type, currentView: UIView?) -> T? {
24 | guard let currentView = currentView else { return nil }
25 |
26 | if let view = currentView as? T {
27 | return view
28 | }
29 |
30 | return recurisiveSearchParentViewUntil(type, currentView: currentView.superview)
31 | }
32 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Alamofire (3.3.1)
3 | - Kingfisher (2.2.2)
4 | - MJRefresh (3.1.0)
5 | - MoyaX (0.0.6):
6 | - Alamofire (~> 3.0)
7 | - p2.OAuth2 (2.2.5):
8 | - SwiftKeychain (~> 1.0)
9 | - SwiftDate (3.0.8)
10 | - SwiftKeychain (1.0.0)
11 | - SwiftyJSON (2.3.2)
12 | - XCGLogger (3.3)
13 |
14 | DEPENDENCIES:
15 | - Alamofire
16 | - Kingfisher
17 | - MJRefresh
18 | - MoyaX
19 | - p2.OAuth2
20 | - SwiftDate
21 | - SwiftyJSON
22 | - XCGLogger
23 |
24 | SPEC CHECKSUMS:
25 | Alamofire: 369bc67b6f5ac33ded3648d7bd21c5bfb91c2ecc
26 | Kingfisher: 7e8de5b0cf7b5dc8ec559d193029a0859df93ef1
27 | MJRefresh: 743e6404967d1c2c688472ea3ecfde247d872db4
28 | MoyaX: 582fb12040dba8d628f180c82204affefecb44e5
29 | p2.OAuth2: 99c2683594a86770f31240d57a162569f1ef0743
30 | SwiftDate: 971f8b9394f664f2226b9c3ab64e801a7291c612
31 | SwiftKeychain: fd9ad69f7b3ff7a0c7ba579da508bbaa89a395b2
32 | SwiftyJSON: 04ccea08915aa0109039157c7974cf0298da292a
33 | XCGLogger: d6e196e78940ff6daab17d6917107876f973fa31
34 |
35 | PODFILE CHECKSUM: 8dbef378b024b5baf4870759bcb858418eeed856
36 |
37 | COCOAPODS: 1.0.0.beta.4
38 |
--------------------------------------------------------------------------------
/RubyChinaApp/Controllers/UserDetail/UserDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDetailViewController.swift
3 | // RubyChinaApp
4 | //
5 | // Created by 姜军 on 12/24/15.
6 | // Copyright © 2015 RubyChina. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class UserDetailViewController: UIViewController {
12 |
13 | // MARK: Properties
14 | var userId: String!
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 | }
19 |
20 | override func didReceiveMemoryWarning() {
21 | super.didReceiveMemoryWarning()
22 | // Dispose of any resources that can be recreated.
23 | }
24 |
25 | override func viewWillAppear(animated: Bool) {
26 |
27 | }
28 |
29 | /*
30 | // MARK: - Navigation
31 |
32 | // In a storyboard-based application, you will often want to do a little preparation before navigation
33 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
34 | // Get the new view controller using segue.destinationViewController.
35 | // Pass the selected object to the new view controller.
36 | }
37 | */
38 | }
39 |
--------------------------------------------------------------------------------
/RubyChinaApp/Controllers/TopicDetail/TopicDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicDetailViewController.swift
3 | // RubyChinaApp
4 | //
5 | // Created by 姜军 on 12/24/15.
6 | // Copyright © 2015 RubyChina. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TopicDetailViewController: UIViewController {
12 |
13 | // MARK: Properties
14 | var topicId: String!
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 | }
19 |
20 | override func didReceiveMemoryWarning() {
21 | super.didReceiveMemoryWarning()
22 | // Dispose of any resources that can be recreated.
23 | }
24 |
25 | override func viewWillAppear(animated: Bool) {
26 |
27 | }
28 |
29 | /*
30 | // MARK: - Navigation
31 |
32 | // In a storyboard-based application, you will often want to do a little preparation before navigation
33 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
34 | // Get the new view controller using segue.destinationViewController.
35 | // Pass the selected object to the new view controller.
36 | }
37 | */
38 | }
39 |
--------------------------------------------------------------------------------
/RubyChinaAppTests/RubyChinaAppTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RubyChinaAppTests.swift
3 | // RubyChinaAppTests
4 | //
5 | // Created by 姜军 on 12/6/15.
6 | // Copyright © 2015 RubyChina. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import RubyChinaApp
11 |
12 | class RubyChinaAppTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | // Use XCTAssert and related functions to verify your tests produce the correct results.
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measureBlock {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Endpoints/Nodes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/17/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | extension RubyChinaV3 {
11 | struct Nodes {
12 | static let Path = "nodes"
13 | }
14 | }
15 |
16 | extension RubyChinaV3.Nodes {
17 | struct Listing: EndpointType {
18 | let baseURL = RubyChinaV3.BaseURL
19 | let path = RubyChinaV3.Nodes.Path
20 |
21 | typealias DeserializedType = [Node]
22 |
23 | func parseResponse(json: JSON) -> DeserializedType? {
24 | return DeserializedType(byJSON: json["nodes"])
25 | }
26 | }
27 |
28 | struct Get: EndpointType {
29 | let id: String
30 |
31 | init(id: String) {
32 | self.id = id
33 | }
34 |
35 | let baseURL = RubyChinaV3.BaseURL
36 | var path: String { return "\(RubyChinaV3.Nodes.Path)/\(self.id)" }
37 |
38 | typealias DeserializedType = Node
39 |
40 | func parseResponse(json: JSON) -> DeserializedType? {
41 | return DeserializedType(byJSON: json["node"])
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/RubyChinaApp/Models/Node.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/7/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import SwiftDate
9 |
10 | struct Node: CustomDebugStringConvertible, ModelType {
11 | let id: String
12 |
13 | let topicsCount: Int
14 |
15 | let sectionId: String
16 | let sectionName: String
17 |
18 | let sort: Int
19 |
20 | let name: String
21 | let summary: String
22 | let updatedAt: NSDate
23 |
24 | var identifier: String {
25 | return "Node#\(self.id)"
26 | }
27 |
28 | init?(byJSON json: JSON) {
29 | if json.type == .Null { return nil }
30 |
31 | self.id = json["id"].stringValue
32 |
33 | self.topicsCount = json["topics_count"].intValue
34 |
35 | self.sectionId = json["section_id"].stringValue
36 | self.sectionName = json["section_name"].stringValue
37 |
38 | self.sort = json["sort"].intValue
39 |
40 | self.name = json["name"].stringValue
41 | self.summary = json["summary"].stringValue
42 | self.updatedAt = json["updated_at"].stringValue.toDate(DateFormat.ISO8601Format(.Extended))!
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Endpoints/Photos.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/16/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | extension RubyChinaV3 {
11 | struct Photos {
12 | static let Path = "photos"
13 |
14 | struct Create: EndpointType {
15 | let data: NSData
16 | let fileName: String
17 |
18 | init(photo: UIKit.UIImage, fileName: String = "uploading_photo.jpg") {
19 | // TODO: Needs optimize
20 | self.data = UIKit.UIImageJPEGRepresentation(photo, 1)!
21 | self.fileName = fileName
22 | }
23 |
24 | let baseURL = RubyChinaV3.BaseURL
25 | let path = "photos"
26 | let method = HTTPMethod.POST
27 | var parameters: [String: Any] {
28 | return ["file": DataForMultipartFormData(data: self.data, fileName: self.fileName, mimeType: "image/jpeg")]
29 | }
30 | let parameterEncoding = ParameterEncoding.MultipartFormData
31 |
32 | typealias DeserializedType = Node
33 |
34 | func parseResponse(json: JSON) -> String? {
35 | return json["image_url"].string
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/RubyChinaAppUITests/RubyChinaAppUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RubyChinaAppUITests.swift
3 | // RubyChinaAppUITests
4 | //
5 | // Created by 姜军 on 12/6/15.
6 | // Copyright © 2015 RubyChina. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class RubyChinaAppUITests: XCTestCase {
12 |
13 | override func setUp() {
14 | super.setUp()
15 |
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 |
18 | // In UI tests it is usually best to stop immediately when a failure occurs.
19 | continueAfterFailure = false
20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
21 | XCUIApplication().launch()
22 |
23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
24 | }
25 |
26 | override func tearDown() {
27 | // Put teardown code here. This method is called after the invocation of each test method in the class.
28 | super.tearDown()
29 | }
30 |
31 | func testExample() {
32 | // Use recording to get started writing UI tests.
33 | // Use XCTAssert and related functions to verify your tests produce the correct results.
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/RubyChinaApp/Models/Reply.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/7/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import SwiftDate
9 |
10 | struct Reply: CustomDebugStringConvertible, ModelType {
11 | let id: String
12 | let topicId: String
13 |
14 | let createdAt: NSDate
15 | let updatedAt: NSDate
16 |
17 | let likesCount: Int
18 |
19 | let isDeleted: Bool
20 |
21 | let bodyHTML: String
22 |
23 | let user: User
24 | let abilities: Ability
25 |
26 | let body: String?
27 |
28 | var isLiked = false
29 |
30 | var identifier: String {
31 | return "Reply#\(self.id)"
32 | }
33 |
34 | init?(byJSON json: JSON) {
35 | if json.type == .Null { return nil }
36 |
37 | self.id = json["id"].stringValue
38 | self.topicId = json["topic_id"].stringValue
39 |
40 | self.createdAt = json["created_at"].stringValue.toDate(DateFormat.ISO8601Format(.Extended))!
41 | self.updatedAt = json["updated_at"].stringValue.toDate(DateFormat.ISO8601Format(.Extended))!
42 |
43 | self.likesCount = json["likes_count"].intValue
44 |
45 | self.isDeleted = json["deleted"].boolValue
46 |
47 | self.bodyHTML = json["body_html"].stringValue
48 |
49 | self.user = User(byJSON: json["user"])!
50 | self.abilities = Ability(byJSON: json["abilities"])!
51 |
52 | // details
53 | self.body = json["body"].string
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/EndpointType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/18/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | protocol EndpointType: Target {
11 | associatedtype DeserializedType
12 |
13 | func parseResponse(json: JSON) -> DeserializedType?
14 | func doRequest(provider: Provider, completion: (result: APIResult) -> ()) -> CancellableToken
15 | }
16 |
17 | extension EndpointType {
18 | func doRequest(provider: Provider = Provider.defaultInstance(), completion: (result: APIResult) -> ()) -> CancellableToken {
19 | return provider.request(self) { result in
20 | var apiResult: APIResult
21 |
22 | switch result {
23 | case let .Response(response):
24 | let json = response.mapSwiftyJSON()
25 |
26 | if json["ok"].int != nil {
27 | apiResult = .Ok
28 | } else if let errorMessage = json["error"].string {
29 | apiResult = .Failure(APIError(statusCode: response.statusCode, message: errorMessage))
30 | } else if let entity = self.parseResponse(json) {
31 | apiResult = .Success(entity)
32 | } else {
33 | apiResult = .Ok
34 | }
35 | case let .Incomplete(error):
36 | apiResult = .NetworkError(error)
37 | }
38 |
39 | completion(result: apiResult)
40 | }
41 | }
42 | }
43 |
44 | extension EndpointType {
45 | func parseResponse(json: JSON) -> DeserializedType? {
46 | return nil
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/RubyChinaApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 0.0.1
19 | CFBundleSignature
20 | ????
21 | CFBundleURLTypes
22 |
23 |
24 | CFBundleTypeRole
25 | Editor
26 | CFBundleURLSchemes
27 |
28 | rbcn
29 |
30 |
31 |
32 | CFBundleVersion
33 | 1
34 | LSRequiresIPhoneOS
35 |
36 | UILaunchStoryboardName
37 | LaunchScreen
38 | UIMainStoryboardFile
39 | Main
40 | UIRequiredDeviceCapabilities
41 |
42 | armv7
43 |
44 | UIStatusBarTintParameters
45 |
46 | UINavigationBar
47 |
48 | Style
49 | UIBarStyleDefault
50 | Translucent
51 |
52 |
53 |
54 | UISupportedInterfaceOrientations
55 |
56 | UIInterfaceOrientationPortrait
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Extensions/CustomDebugStringConvertible.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/12/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | extension CustomDebugStringConvertible {
9 | var debugDescription: String {
10 | let mirror = Mirror(reflecting: self)
11 |
12 | var children = [(label: String?, value: Any)]()
13 | let mirrorChildrenCollection = AnyRandomAccessCollection(mirror.children)!
14 | children += mirrorChildrenCollection
15 |
16 | var currentMirror = mirror
17 | while let superclassChildren = currentMirror.superclassMirror()?.children {
18 | let randomCollection = AnyRandomAccessCollection(superclassChildren)!
19 | children += randomCollection
20 | currentMirror = currentMirror.superclassMirror()!
21 | }
22 |
23 | var filteredChildren = [(label: String?, value: Any)]()
24 | for (optionalPropertyName, value) in children {
25 | if !optionalPropertyName!.containsString("notMapped_") {
26 | filteredChildren += [(optionalPropertyName, value)]
27 | }
28 | }
29 |
30 | var index = 0
31 |
32 | var str = "<\(String(mirror.subjectType))"
33 |
34 | for (optionalPropertyName, value) in filteredChildren {
35 | let propertyName = optionalPropertyName!
36 |
37 | str += " \(propertyName)="
38 |
39 | if let propertyValue = value as? CustomDebugStringConvertible {
40 | str += propertyValue.debugDescription
41 | } else {
42 | let propertyValue = String(value)
43 |
44 | if propertyValue == "nil" {
45 | str += "nil"
46 | } else {
47 | str += propertyValue
48 | }
49 | }
50 |
51 | index += 1
52 | }
53 |
54 | str += ">"
55 |
56 | return str
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/RubyChinaApp/Controllers/Topics/TopicCellViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 1/7/16.
3 | // Copyright (c) 2016 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import UIKit
8 | import Kingfisher
9 |
10 | struct TopicCellViewModel: ViewModelType {
11 | let id: String
12 | let title: String
13 | let repliesCount: Int
14 | let nodeName: String
15 | let authorName: String
16 | let authorAvatarURL: NSURL
17 |
18 | init(byEntity entity: Topic) {
19 | self.id = entity.id
20 | self.title = entity.title
21 | self.repliesCount = entity.repliesCount
22 | self.nodeName = entity.nodeName
23 | self.authorName = entity.user.login
24 | self.authorAvatarURL = entity.user.avatarUrl
25 | }
26 |
27 | func applyTo(cell: TopicsTableViewCell) {
28 | cell.titleLabel.text = self.title
29 | cell.repliesCountLabel.text = String(self.repliesCount)
30 |
31 | cell.authorNameButton.setTitle(self.authorName, forState: .Normal)
32 | cell.authorNameButton.titleLabel!.adjustsFontSizeToFitWidth = true
33 |
34 | cell.nodeNameButton.setTitle(self.nodeName, forState: .Normal)
35 | cell.nodeNameButton.titleLabel!.adjustsFontSizeToFitWidth = true
36 |
37 | cell.authorAvatarImageButton.kf_setImageWithURL(self.authorAvatarURL,
38 | forState: .Normal,
39 | placeholderImage: nil,
40 | optionsInfo: [.Transition(ImageTransition.Fade(0.1))])
41 | }
42 |
43 | func applyTo(viewController: TopicDetailViewController) {
44 | viewController.topicId = self.id
45 | viewController.title = self.title
46 | }
47 | }
48 |
49 | func toTopicCellViewModel(topics: [Topic]) -> [TopicCellViewModel] {
50 | var viewModels = [TopicCellViewModel]()
51 | for topic in topics {
52 | viewModels.append(TopicCellViewModel(byEntity: topic))
53 | }
54 |
55 | return viewModels
56 | }
57 |
--------------------------------------------------------------------------------
/RubyChinaApp/Controllers/FirstViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirstViewController.swift
3 | // RubyChinaApp
4 | //
5 | // Created by 姜军 on 12/6/15.
6 | // Copyright © 2015 RubyChina. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class FirstViewController: UIViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 | }
16 |
17 | @IBAction func SignOut(sender: UIButton) {
18 | provider.resetAuthorize()
19 | }
20 |
21 | @IBAction func SendHello(sender: UIButton) {
22 | RubyChinaV3.Topics.Get(id: "283541").doRequest() { result in
23 | switch result {
24 | case let .Success(topic):
25 | dump(topic)
26 | case let .Failure(error):
27 | dump(error)
28 | default:
29 | dump(result)
30 | }
31 | }
32 |
33 | RubyChinaV3.Replies.Listing(topicId: "283541").doRequest() { result in
34 | switch result {
35 | case let .Success(replies):
36 | dump(replies)
37 | case let .Failure(error):
38 | dump(error)
39 | default:
40 | dump(result)
41 | }
42 | }
43 | }
44 |
45 | @IBAction func createPost(sender: UIButton) {
46 | let title = "yoooooo"
47 | let body = "*小朋友们大家好*\n\n# 还记得我是谁么\n\n- 对了\n- 我就是给蓝猫配音的演员\n\n 葛炮!"
48 | let nodeId = "61"
49 |
50 | RubyChinaV3.Topics.Create(title: title, body: body, nodeId: nodeId).doRequest() { result in
51 | switch result {
52 | case let .Success(entity):
53 | dump(entity)
54 |
55 | default:
56 | dump(result)
57 | }
58 | }
59 | }
60 |
61 | @IBAction func signInEmbedded(sender: UIButton) {
62 | signIn(sender)
63 | }
64 |
65 | func signIn(sender: UIButton) {
66 | provider.authorizeContext = self
67 | provider.authorize()
68 | }
69 |
70 | override func didReceiveMemoryWarning() {
71 | super.didReceiveMemoryWarning()
72 | // Dispose of any resources that can be recreated.
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/OffsetPager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 1/24/16.
3 | // Copyright (c) 2016 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import MoyaX
8 |
9 | protocol OffsetPaginatable {
10 | var offset: Int { get set }
11 | var limit: Int { get set }
12 |
13 | associatedtype DeserializedType: CollectionType
14 | }
15 |
16 | // Abstract class
17 | class OffsetPager> {
18 | let perPage: Int
19 |
20 | var currentPage: Int {
21 | didSet {
22 | self.endpoint.offset = self.currentOffset
23 | }
24 | }
25 | var currentOffset: Int {
26 | return (self.currentPage - 1) * self.perPage
27 | }
28 |
29 | private(set) var isNoMoreData: Bool = false
30 |
31 | private var endpoint: T
32 |
33 | init(endpoint: T, withPage page: Int = 1, withPerPage perPage: Int = 20) {
34 | self.perPage = perPage
35 | self.currentPage = page
36 |
37 | self.endpoint = endpoint
38 | self.endpoint.offset = self.currentOffset
39 | self.endpoint.limit = perPage
40 | }
41 |
42 | func loadFresh(completion: (result: APIResult) -> ()) -> CancellableToken {
43 | self.currentPage = 1
44 | self.isNoMoreData = false
45 |
46 | return self.endpoint.doRequest() { result in
47 | if case .Success(let entities) = result {
48 | if entities.isEmpty {
49 | self.isNoMoreData = true
50 | } else {
51 | self.currentPage += 1
52 | }
53 | }
54 |
55 | completion(result: result)
56 | }
57 | }
58 |
59 | func loadMore(completion: (result: APIResult) -> ()) -> CancellableToken {
60 | return self.endpoint.doRequest() { result in
61 | if case .Success(let entities) = result {
62 | if entities.isEmpty {
63 | self.isNoMoreData = true
64 | } else {
65 | self.currentPage += 1
66 | }
67 | }
68 |
69 | completion(result: result)
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/RubyChinaApp/Models/Topic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/7/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import SwiftDate
9 |
10 | struct Topic: CustomDebugStringConvertible, ModelType {
11 | let id: String
12 |
13 | let createdAt: NSDate
14 | let updatedAt: NSDate
15 | let repliedAt: NSDate?
16 |
17 | let repliesCount: Int
18 |
19 | let nodeName: String
20 | let nodeId: String
21 |
22 | let lastReplyUserId: String?
23 | let lastReplyUserLogin: String?
24 |
25 | let isExcellent: Bool
26 | let isDeleted: Bool
27 |
28 | let title: String
29 |
30 | let user: User
31 | let abilities: Ability
32 |
33 | let suggestedAt: NSDate?
34 | let hits: Int?
35 | let likesCount: Int?
36 | let body: String?
37 | let bodyHTML: String?
38 |
39 | var isFollowed = false
40 | var isLiked = false
41 | var isFavorited = false
42 |
43 | var identifier: String {
44 | return "Topic#\(self.id)"
45 | }
46 |
47 | init?(byJSON json: JSON) {
48 | if json.type == .Null { return nil }
49 |
50 | self.id = json["id"].stringValue
51 |
52 | self.createdAt = json["created_at"].stringValue.toDate(DateFormat.ISO8601Format(.Extended))!
53 | self.updatedAt = json["updated_at"].stringValue.toDate(DateFormat.ISO8601Format(.Extended))!
54 | self.repliedAt = json["replied_at"].string?.toDate(DateFormat.ISO8601Format(.Extended))
55 |
56 | self.repliesCount = json["replies_count"].intValue
57 |
58 | self.nodeName = json["node_name"].stringValue
59 | self.nodeId = json["node_id"].stringValue
60 |
61 | self.lastReplyUserId = json["last_reply_user_id"].string
62 | self.lastReplyUserLogin = json["last_reply_user_login"].string
63 |
64 | self.isExcellent = json["excellent"].boolValue
65 | self.isDeleted = json["deleted"].boolValue
66 |
67 | self.title = json["title"].stringValue
68 |
69 | self.user = User(byJSON: json["user"])!
70 | self.abilities = Ability(byJSON: json["abilities"])!
71 |
72 | // details
73 | self.suggestedAt = json["suggested_at"].string?.toDate(DateFormat.ISO8601Format(.Extended))
74 | self.hits = json["hits"].int
75 | self.likesCount = json["likes_count"].int
76 | self.body = json["body"].string
77 | self.bodyHTML = json["body_html"].string
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/RubyChinaApp/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // RubyChinaApp
4 | //
5 | // Created by 姜军 on 12/6/15.
6 | // Copyright © 2015 RubyChina. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Kingfisher
11 |
12 | @UIApplicationMain
13 | class AppDelegate: UIResponder, UIApplicationDelegate {
14 | var window: UIWindow?
15 |
16 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
17 | // Override point for customization after application launch.
18 | logger.setup(GlobalConstant.logLevel, showThreadName: true, showLogLevel: true, showFileNames: false, showLineNumbers: false, writeToFile: nil, fileLogLevel: .Debug)
19 |
20 | let downloader = KingfisherManager.sharedManager.downloader
21 |
22 | // Download process will timeout after 5 seconds. Default is 15.
23 | downloader.downloadTimeout = 5
24 |
25 | return true
26 | }
27 |
28 | func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
29 | dump(url)
30 |
31 | provider.handleRedirectURL(url)
32 |
33 | return true
34 | }
35 |
36 | func applicationWillResignActive(application: UIApplication) {
37 | // 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.
38 | // 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.
39 | }
40 |
41 | func applicationDidEnterBackground(application: UIApplication) {
42 | // 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.
43 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
44 | }
45 |
46 | func applicationWillEnterForeground(application: UIApplication) {
47 | // 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.
48 | }
49 |
50 | func applicationDidBecomeActive(application: UIApplication) {
51 | // 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.
52 | }
53 |
54 | func applicationWillTerminate(application: UIApplication) {
55 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/RubyChinaApp/Models/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/7/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import SwiftDate
9 |
10 | struct User: CustomDebugStringConvertible, ModelType {
11 | let id: String
12 |
13 | let login: String
14 | let name: String?
15 | let avatarUrl: NSURL
16 |
17 | let location: String?
18 | let company: String?
19 | let twitter: String?
20 | let website: NSURL?
21 | let bio: String?
22 | let tagline: String?
23 | let github: String?
24 | let email: String?
25 |
26 | let topicsCount: Int?
27 | let repliesCount: Int?
28 | let followingCount: Int?
29 | let followersCount: Int?
30 | let favoritesCount: Int?
31 |
32 | let level: LevelValue?
33 | let levelName: String?
34 |
35 | let createdAt: NSDate?
36 |
37 | var isFollowed = false
38 | var isBlocked = false
39 |
40 | var identifier: String {
41 | return "User#\(self.id)"
42 | }
43 |
44 | init?(byJSON json: JSON) {
45 | if json.type == .Null { return nil }
46 |
47 | self.id = json["id"].stringValue
48 |
49 | self.login = json["login"].stringValue
50 | self.name = json["name"].string
51 | self.avatarUrl = NSURL(string: json["avatar_url"].stringValue)!
52 |
53 | // details
54 | self.location = json["location"].string
55 | self.company = json["company"].string
56 | self.twitter = json["twitter"].string
57 | if let website = json["website"].string {
58 | self.website = NSURL(string: website)
59 | } else {
60 | self.website = nil
61 | }
62 | self.bio = json["bio"].string
63 | self.tagline = json["tagline"].string
64 | self.github = json["github"].string
65 | self.email = json["email"].string
66 |
67 | self.topicsCount = json["topics_count"].int
68 | self.repliesCount = json["replies_count"].int
69 | self.followingCount = json["following_count"].int
70 | self.followersCount = json["followers_count"].int
71 | self.favoritesCount = json["favorites_count"].int
72 |
73 | self.level = LevelValue(byJSON: json["level"])
74 | self.levelName = json["level_name"].string
75 |
76 | self.createdAt = json["created_at"].string?.toDate(DateFormat.ISO8601Format(.Extended))
77 | }
78 | }
79 |
80 | extension User: Hashable, Equatable {
81 | internal var hashValue: Int {
82 | return "User#\(self.id)".hashValue
83 | }
84 | }
85 |
86 | func ==(lhs: User, rhs: User) -> Bool {
87 | return lhs.id == rhs.id
88 | }
89 |
90 | extension User {
91 | enum LevelValue: String, CustomDebugStringConvertible, SwiftyJSONMappable {
92 | case Admin = "admin"
93 | case VIP = "vip"
94 | case HR = "hr"
95 | case Blocked = "blocked"
96 | case Newbie = "newbie"
97 | case Normal = "normal"
98 |
99 | init?(byJSON json: JSON) {
100 | if json.type == .Null { return nil }
101 |
102 | if let value = self.dynamicType.init(rawValue: json.stringValue) {
103 | self = value
104 | } else {
105 | self = .Normal
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Provider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/20/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import MoyaX
8 | import p2_OAuth2
9 | import Alamofire
10 |
11 | class Provider {
12 | var timeOut: NSTimeInterval = 15
13 |
14 | private var oauthClient: OAuth2CodeGrant!
15 | private var provider: MoyaXProvider!
16 |
17 | var afterAuthorizeSuccess: ((parameters: OAuth2JSON) -> Void)? {
18 | get { return oauthClient.onAuthorize }
19 | set(closure) { oauthClient.onAuthorize = closure }
20 | }
21 | var afterAuthorizeFailure: ((error: ErrorType?) -> Void)? {
22 | get { return oauthClient.onFailure }
23 | set(closure) { oauthClient.onFailure = closure }
24 | }
25 | var afterAuthorizeSuccessOrFailure: ((wasFailure: Bool, error: ErrorType?) -> Void)? {
26 | get { return oauthClient.afterAuthorizeOrFailure }
27 | set(closure) { oauthClient.afterAuthorizeOrFailure = closure }
28 | }
29 |
30 | var authorizeContext: AnyObject? {
31 | get { return oauthClient.authConfig.authorizeContext }
32 | set(context) { oauthClient.authConfig.authorizeContext = context }
33 | }
34 |
35 | init(clientID: String, clientSecret: String, redirect_uris: [String],
36 | scope: String = "",
37 | authorizeEmbedded: Bool = true,
38 | middlewares: [Middleware] = []) {
39 | oauthClient = OAuth2CodeGrant(settings: [
40 | "client_id": clientID,
41 | "client_secret": clientSecret,
42 | "authorize_uri": "https://ruby-china.org/oauth/authorize",
43 | "token_uri": "https://ruby-china.org/oauth/token",
44 | "scope": scope,
45 | "redirect_uris": redirect_uris,
46 | "secret_in_body": false,
47 | "verbose": true,
48 | ] as OAuth2JSON)
49 |
50 | let prepareForEndpointClosure: Endpoint -> () = { endpoint in
51 | // Signing
52 | if let accessToken = self.oauthClient!.clientConfig.accessToken where !accessToken.isEmpty {
53 | endpoint.headerFields["Authorization"] = "Bearer \(accessToken)"
54 | }
55 | }
56 |
57 | provider = MoyaXProvider(backend: AlamofireBackend(manager: alamofireManager(self.timeOut)),
58 | prepareForEndpoint: prepareForEndpointClosure,
59 | middlewares: middlewares)
60 |
61 | oauthClient.authConfig.authorizeEmbedded = authorizeEmbedded
62 | }
63 |
64 | func request(target: Target, completion: MoyaX.Completion) -> CancellableToken {
65 | return provider.request(target, completion: completion)
66 | }
67 |
68 | func authorize(params: OAuth2StringDict? = nil) {
69 | oauthClient.authorize(params: params)
70 | }
71 |
72 | func resetAuthorize() {
73 | oauthClient.forgetTokens()
74 | }
75 |
76 | func handleRedirectURL(redirect: NSURL) {
77 | oauthClient.handleRedirectURL(redirect)
78 | }
79 |
80 | func isAuthorized() -> Bool {
81 | if let accessToken = self.oauthClient!.clientConfig.accessToken where !accessToken.isEmpty {
82 | return true
83 | } else {
84 | return false
85 | }
86 | }
87 | }
88 |
89 | func alamofireManager(timeOut: NSTimeInterval = 15) -> Manager {
90 | let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
91 | configuration.HTTPAdditionalHeaders = Manager.defaultHTTPHeaders
92 | configuration.timeoutIntervalForRequest = timeOut
93 | configuration.timeoutIntervalForResource = timeOut
94 |
95 | let manager = Manager(configuration: configuration)
96 | return manager
97 | }
98 |
--------------------------------------------------------------------------------
/RubyChinaApp/Views/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Endpoints/Replies.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/17/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | extension RubyChinaV3 {
11 | struct Replies {
12 | static let Path = "replies"
13 | }
14 | }
15 |
16 | extension RubyChinaV3.Replies {
17 | struct Listing: EndpointType, OffsetPaginatable {
18 | let topicId: String
19 | var offset: Int
20 | var limit: Int
21 |
22 | init(topicId: String, offset: Int = 0, limit: Int = 20) {
23 | self.topicId = topicId
24 | self.offset = offset
25 | self.limit = limit
26 | }
27 |
28 | let baseURL = RubyChinaV3.BaseURL
29 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.topicId)/\(RubyChinaV3.Replies.Path)" }
30 | var parameters: [String: AnyObject] {
31 | var parameters = [String: AnyObject]()
32 |
33 | parameters["limit"] = self.limit
34 | parameters["offset"] = self.offset
35 |
36 | return parameters
37 | }
38 |
39 | typealias DeserializedType = [Reply]
40 |
41 | func parseResponse(json: JSON) -> DeserializedType? {
42 | var topics = DeserializedType(byJSON: json["replies"])
43 |
44 | if let userLikedReplyIDs = json["meta"]["user_liked_reply_ids"].array?.map({ $0.stringValue }) where !userLikedReplyIDs.isEmpty {
45 | for (index, topic) in topics.enumerate() {
46 | if userLikedReplyIDs.contains(topic.id) {
47 | topics[index].isLiked = true
48 | }
49 | }
50 | }
51 |
52 | return topics
53 | }
54 | }
55 |
56 | struct Create: EndpointType {
57 | let topicId: String
58 | let body: String
59 |
60 | init(topicId: String, body: String) {
61 | self.topicId = topicId
62 | self.body = body
63 | }
64 |
65 | let baseURL = RubyChinaV3.BaseURL
66 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.topicId)/\(RubyChinaV3.Replies.Path)" }
67 | let method = HTTPMethod.POST
68 | var parameters: [String: AnyObject] {
69 | var parameters = [String: AnyObject]()
70 |
71 | parameters["body"] = self.body
72 |
73 | return parameters
74 | }
75 |
76 | typealias DeserializedType = Reply
77 |
78 | func parseResponse(json: JSON) -> DeserializedType? {
79 | return DeserializedType(byJSON: json["reply"])
80 | }
81 | }
82 |
83 | struct Get: EndpointType {
84 | let id: String
85 |
86 | init(id: String) {
87 | self.id = id
88 | }
89 |
90 | let baseURL = RubyChinaV3.BaseURL
91 | var path: String { return "\(RubyChinaV3.Replies.Path)/\(self.id)" }
92 |
93 | typealias DeserializedType = Reply
94 |
95 | func parseResponse(json: JSON) -> DeserializedType? {
96 | return DeserializedType(byJSON: json["reply"])
97 | }
98 | }
99 |
100 | struct Destroy: EndpointType {
101 | let id: String
102 |
103 | init(id: String) {
104 | self.id = id
105 | }
106 |
107 | let baseURL = RubyChinaV3.BaseURL
108 | var path: String { return "\(RubyChinaV3.Replies.Path)/\(self.id)" }
109 | let method = HTTPMethod.DELETE
110 |
111 | typealias DeserializedType = AnyObject
112 | }
113 |
114 | struct Update: EndpointType {
115 | let id: String
116 | let body: String
117 |
118 | init(id: String, body: String) {
119 | self.id = id
120 | self.body = body
121 | }
122 |
123 | let baseURL = RubyChinaV3.BaseURL
124 | var path: String { return "\(RubyChinaV3.Replies.Path)/\(self.id)" }
125 | let method = HTTPMethod.PUT
126 | var parameters: [String: AnyObject] {
127 | var parameters = [String: AnyObject]()
128 |
129 | parameters["body"] = self.body
130 |
131 | return parameters
132 | }
133 |
134 | typealias DeserializedType = Reply
135 |
136 | func parseResponse(json: JSON) -> DeserializedType? {
137 | return DeserializedType(byJSON: json["reply"])
138 | }
139 | }
140 |
141 | struct Like: EndpointType {
142 | let id: String
143 |
144 | init(id: String) {
145 | self.id = id
146 | }
147 |
148 | let baseURL = RubyChinaV3.BaseURL
149 | let path = "likes"
150 | let method = HTTPMethod.POST
151 | var parameters: [String: AnyObject] {
152 | var parameters = [String: AnyObject]()
153 |
154 | parameters["obj_type"] = "reply"
155 | parameters["obj_id"] = self.id
156 |
157 | return parameters
158 | }
159 |
160 | typealias DeserializedType = AnyObject
161 | }
162 |
163 | struct UnLike: EndpointType {
164 | let id: String
165 |
166 | init(id: String) {
167 | self.id = id
168 | }
169 |
170 | let baseURL = RubyChinaV3.BaseURL
171 | let path = "likes"
172 | let method = HTTPMethod.DELETE
173 | var parameters: [String: AnyObject] {
174 | var parameters = [String: AnyObject]()
175 |
176 | parameters["obj_type"] = "reply"
177 | parameters["obj_id"] = self.id
178 |
179 | return parameters
180 | }
181 |
182 | typealias DeserializedType = AnyObject
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/RubyChinaApp/Controllers/Topics/TopicsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicsViewController.swift
3 | // RubyChinaApp
4 | //
5 | // Created by 姜军 on 12/24/15.
6 | // Copyright © 2015 RubyChina. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import MJRefresh
11 |
12 | class TopicsViewController: UIViewController, SegueHandlerType {
13 |
14 | enum SegueIdentifier: String {
15 | case ShowTopicDetailSegue = "ShowTopicDetailSegue"
16 | case ShowUserDetailSegue = "ShowUserDetailSegue"
17 | }
18 |
19 | // MARK: Properties
20 | var topicsPager = TopicsPager(withPerPage: 10)
21 | var topicViewModels = [TopicCellViewModel]()
22 |
23 | @IBOutlet weak var topicsTableView: UITableView!
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 |
28 | self.topicsTableView.delegate = self
29 | self.topicsTableView.dataSource = self
30 |
31 | self.topicsTableView.separatorColor = UIColor.clearColor()
32 |
33 | self.topicsTableView.estimatedRowHeight = 72
34 | self.topicsTableView.rowHeight = UITableViewAutomaticDimension
35 |
36 | let tableViewHeader = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(TopicsViewController.pullDownRefreshingAction))
37 | tableViewHeader.lastUpdatedTimeLabel!.hidden = true
38 |
39 | self.topicsTableView.mj_header = tableViewHeader
40 |
41 | let tableViewFooter = MJRefreshAutoNormalFooter(refreshingTarget: self, refreshingAction: #selector(TopicsViewController.pullUpRefreshingAction))
42 |
43 | self.topicsTableView.mj_footer = tableViewFooter
44 |
45 | self.topicsTableView.mj_header.beginRefreshing()
46 |
47 | // Uncomment the following line to preserve selection between presentations
48 | // self.clearsSelectionOnViewWillAppear = false
49 |
50 | // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
51 | // self.navigationItem.rightBarButtonItem = self.editButtonItem()
52 | }
53 |
54 | override func didReceiveMemoryWarning() {
55 | super.didReceiveMemoryWarning()
56 | // Dispose of any resources that can be recreated.
57 | }
58 |
59 | // MARK: - Navigation
60 |
61 | // In a storyboard-based application, you will often want to do a little preparation before navigation
62 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
63 | guard let sender = sender as? UIView else {
64 | fatalError("Sender is nil")
65 | }
66 |
67 | guard let index = selectedRowAtIndexPathFor(self.topicsTableView, sender: sender) else {
68 | fatalError("Invalid index")
69 | }
70 |
71 | let identifier = segueIdentifierForSegue(segue)
72 | let viewModel = self.topicViewModels[index]
73 |
74 | switch identifier {
75 | case .ShowTopicDetailSegue:
76 | let destination = segue.destinationViewController as! TopicDetailViewController
77 | viewModel.applyTo(destination)
78 | case .ShowUserDetailSegue:
79 | let destination = segue.destinationViewController as! UIViewController
80 | destination.title = viewModel.authorName
81 | }
82 | }
83 | }
84 |
85 | extension TopicsViewController: UITableViewDataSource {
86 | // MARK: - Table view data source
87 |
88 | func numberOfSectionsInTableView(tableView: UITableView) -> Int {
89 | return 1
90 | }
91 |
92 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
93 | return self.topicViewModels.count
94 | }
95 |
96 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
97 | let cell = tableView.dequeueReusableCellWithIdentifier(String(TopicsTableViewCell), forIndexPath: indexPath) as! TopicsTableViewCell
98 |
99 | return cell
100 | }
101 | }
102 |
103 | extension TopicsViewController: UITableViewDelegate {
104 | // MARK: - Table view delegation
105 |
106 | func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
107 | let topicCell = cell as! TopicsTableViewCell
108 | let viewModel = self.topicViewModels[indexPath.row]
109 |
110 | viewModel.applyTo(topicCell)
111 | }
112 |
113 | func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
114 | defer {
115 | tableView.deselectRowAtIndexPath(indexPath, animated: true)
116 | }
117 | }
118 | }
119 |
120 | extension TopicsViewController {
121 | func pullDownRefreshingAction() {
122 | defer {
123 | self.topicsTableView.mj_header.endRefreshing()
124 | }
125 |
126 | self.topicsPager.loadFresh() {
127 | result in
128 | switch result {
129 | case let .Success(topics):
130 | self.topicViewModels = toTopicCellViewModel(topics)
131 |
132 | self.topicsTableView.reloadData()
133 | case let .Failure(error):
134 | dump(error)
135 | default:
136 | dump(result)
137 | }
138 | }
139 | }
140 |
141 | func pullUpRefreshingAction() {
142 | defer {
143 | self.topicsTableView.mj_footer.endRefreshing()
144 | }
145 |
146 | if self.topicsPager.isNoMoreData {
147 | return
148 | }
149 |
150 | self.topicsPager.loadMore {
151 | result in
152 | switch result {
153 | case let .Success(topics):
154 | self.topicViewModels.appendContentsOf(toTopicCellViewModel(topics))
155 |
156 | self.topicsTableView.reloadData()
157 | case let .Failure(error):
158 | dump(error)
159 | default:
160 | dump(result)
161 | }
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Endpoints/Topics.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/14/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | extension RubyChinaV3 {
11 | struct Topics {
12 | static let Path = "topics"
13 | }
14 | }
15 |
16 | extension RubyChinaV3.Topics {
17 | struct Listing: EndpointType, OffsetPaginatable {
18 | enum TypeFieldValue: String {
19 | case LastActived = "last_actived"
20 | case Recent = "recent"
21 | case NoReply = "no_reply"
22 | case Popular = "popular"
23 | case Excellent = "excellent"
24 | }
25 |
26 | let type: TypeFieldValue?
27 | let nodeId: String?
28 | var offset: Int
29 | var limit: Int
30 |
31 | init(type: TypeFieldValue? = nil, nodeId: String? = nil, offset: Int = 0, limit: Int = 20) {
32 | self.type = type
33 | self.nodeId = nodeId
34 | self.offset = offset
35 | self.limit = limit
36 | }
37 |
38 | let baseURL = RubyChinaV3.BaseURL
39 | let path = RubyChinaV3.Topics.Path
40 | var parameters: [String: AnyObject] {
41 | var parameters = [String: AnyObject]()
42 |
43 | if let type = self.type {
44 | parameters["type"] = type.rawValue
45 | }
46 | if let nodeId = self.nodeId {
47 | parameters["nodeId"] = nodeId
48 | }
49 |
50 | parameters["limit"] = self.limit
51 | parameters["offset"] = self.offset
52 |
53 | return parameters
54 | }
55 |
56 | typealias DeserializedType = [Topic]
57 |
58 | func parseResponse(json: JSON) -> DeserializedType? {
59 | return DeserializedType(byJSON: json["topics"])
60 | }
61 | }
62 |
63 | struct Get: EndpointType {
64 | let id: String
65 |
66 | init(id: String) {
67 | self.id = id
68 | }
69 |
70 | let baseURL = RubyChinaV3.BaseURL
71 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)" }
72 |
73 | typealias DeserializedType = Topic
74 |
75 | func parseResponse(json: JSON) -> DeserializedType? {
76 | if json["topic"].type == .Null {
77 | return nil
78 | }
79 |
80 | var topic = DeserializedType(byJSON: json["topic"])!
81 |
82 | if let isFollowed = json["meta"]["followed"].bool {
83 | topic.isFollowed = isFollowed
84 | }
85 | if let isLiked = json["meta"]["liked"].bool {
86 | topic.isLiked = isLiked
87 | }
88 | if let isFavorited = json["meta"]["favorited"].bool {
89 | topic.isFavorited = isFavorited
90 | }
91 |
92 | return topic
93 | }
94 | }
95 |
96 | struct Create: EndpointType {
97 | let nodeId: String
98 | let title: String
99 | let body: String
100 |
101 | init(title: String, body: String, nodeId: String) {
102 | self.title = title
103 | self.body = body
104 | self.nodeId = nodeId
105 | }
106 |
107 | let baseURL = RubyChinaV3.BaseURL
108 | let path = RubyChinaV3.Topics.Path
109 | let method = HTTPMethod.POST
110 | var parameters: [String: AnyObject] {
111 | var parameters = [String: AnyObject]()
112 |
113 | parameters["title"] = self.title
114 | parameters["body"] = self.body
115 | parameters["node_id"] = self.nodeId
116 |
117 | return parameters
118 | }
119 |
120 | typealias DeserializedType = Topic
121 |
122 | func parseResponse(json: JSON) -> DeserializedType? {
123 | return DeserializedType(byJSON: json["topic"])
124 | }
125 | }
126 |
127 | struct Update: EndpointType {
128 | let id: String
129 | let nodeId: String
130 | let title: String
131 | let body: String
132 |
133 | init(id: String, title: String, body: String, nodeId: String) {
134 | self.id = id
135 | self.title = title
136 | self.body = body
137 | self.nodeId = nodeId
138 | }
139 |
140 | let baseURL = RubyChinaV3.BaseURL
141 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)" }
142 | let method = HTTPMethod.PUT
143 | var parameters: [String: AnyObject] {
144 | var parameters = [String: AnyObject]()
145 |
146 | parameters["title"] = self.title
147 | parameters["body"] = self.body
148 | parameters["node_id"] = self.nodeId
149 |
150 | return parameters
151 | }
152 |
153 | typealias DeserializedType = Topic
154 |
155 | func parseResponse(json: JSON) -> DeserializedType? {
156 | return DeserializedType(byJSON: json["topic"])
157 | }
158 | }
159 |
160 | struct Destroy: EndpointType {
161 | var id: String
162 |
163 | init(id: String) {
164 | self.id = id
165 | }
166 |
167 | let baseURL = RubyChinaV3.BaseURL
168 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)" }
169 | let method = HTTPMethod.DELETE
170 |
171 | typealias DeserializedType = AnyObject
172 | }
173 |
174 | struct Like: EndpointType {
175 | var id: String
176 |
177 | init(id: String) {
178 | self.id = id
179 | }
180 |
181 | let baseURL = RubyChinaV3.BaseURL
182 | let path = "likes"
183 | let method = HTTPMethod.POST
184 | var parameters: [String: AnyObject] {
185 | var parameters = [String: AnyObject]()
186 |
187 | parameters["obj_type"] = "topic"
188 | parameters["obj_id"] = self.id
189 |
190 | return parameters
191 | }
192 |
193 | typealias DeserializedType = AnyObject
194 | }
195 |
196 | struct UnLike: EndpointType {
197 | let id: String
198 |
199 | init(id: String) {
200 | self.id = id
201 | }
202 |
203 | let baseURL = RubyChinaV3.BaseURL
204 | let path = "likes"
205 | let method = HTTPMethod.DELETE
206 | var parameters: [String: AnyObject] {
207 | var parameters = [String: AnyObject]()
208 |
209 | parameters["obj_type"] = "topic"
210 | parameters["obj_id"] = self.id
211 |
212 | return parameters
213 | }
214 |
215 | typealias DeserializedType = AnyObject
216 | }
217 |
218 | struct Favorite: EndpointType {
219 | var id: String
220 |
221 | init(id: String) {
222 | self.id = id
223 | }
224 |
225 | let baseURL = RubyChinaV3.BaseURL
226 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)/favorite" }
227 | let method = HTTPMethod.POST
228 |
229 | typealias DeserializedType = AnyObject
230 | }
231 |
232 | struct UnFavorite: EndpointType {
233 | var id: String
234 |
235 | init(id: String) {
236 | self.id = id
237 | }
238 |
239 | let baseURL = RubyChinaV3.BaseURL
240 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)/unfavorite" }
241 | var method = HTTPMethod.POST
242 |
243 | typealias DeserializedType = AnyObject
244 | }
245 |
246 | struct Follow: EndpointType {
247 | let id: String
248 |
249 | init(id: String) {
250 | self.id = id
251 | }
252 |
253 | let baseURL = RubyChinaV3.BaseURL
254 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)/follow" }
255 | let method = HTTPMethod.POST
256 |
257 | typealias DeserializedType = AnyObject
258 | }
259 |
260 | struct UnFollow: EndpointType {
261 | var id: String
262 |
263 | init(id: String) {
264 | self.id = id
265 | }
266 |
267 | let baseURL = RubyChinaV3.BaseURL
268 | var path: String { return "\(RubyChinaV3.Topics.Path)/\(self.id)/unfollow" }
269 | let method = HTTPMethod.POST
270 |
271 | typealias DeserializedType = AnyObject
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/RubyChinaApp/Libraries/Endpoints/Users.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 姜军 on 12/17/15.
3 | // Copyright (c) 2015 RubyChina. All rights reserved.
4 | //
5 |
6 | import Foundation
7 | import SwiftyJSON
8 | import MoyaX
9 |
10 | extension RubyChinaV3 {
11 | struct Users {
12 | static let Path = "users"
13 | }
14 | }
15 |
16 | extension RubyChinaV3.Users {
17 | struct Listing: EndpointType {
18 | let limit: Int?
19 |
20 | init(limit: Int? = nil) {
21 | self.limit = limit
22 | }
23 |
24 | let baseURL = RubyChinaV3.BaseURL
25 | let path = RubyChinaV3.Users.Path
26 | var parameters: [String: AnyObject] {
27 | var parameters = [String: AnyObject]()
28 |
29 | if let limit = self.limit {
30 | parameters["limit"] = limit
31 | }
32 |
33 | return parameters
34 | }
35 |
36 | typealias DeserializedType = [User]
37 |
38 | func parseResponse(json: JSON) -> DeserializedType? {
39 | return DeserializedType(byJSON: json["users"])
40 | }
41 | }
42 |
43 | struct Get: EndpointType {
44 | let login: String
45 |
46 | init(login: String) {
47 | self.login = login
48 | }
49 |
50 | let baseURL = RubyChinaV3.BaseURL
51 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)" }
52 |
53 | typealias DeserializedType = User
54 |
55 | func parseResponse(json: JSON) -> DeserializedType? {
56 | if json["user"].type == .Null {
57 | return nil
58 | }
59 |
60 | var user = DeserializedType(byJSON: json["user"])!
61 |
62 | if let isFollowed = json["meta"]["followed"].bool {
63 | user.isFollowed = isFollowed
64 | }
65 | if let isBlocked = json["meta"]["blocked"].bool {
66 | user.isBlocked = isBlocked
67 | }
68 |
69 | return user
70 | }
71 | }
72 |
73 | struct Topics: EndpointType, OffsetPaginatable {
74 | enum OrderBy: String {
75 | case Recent = "recent"
76 | case Likes = "likes"
77 | case Replies = "replies"
78 | }
79 |
80 | let login: String
81 | let orderBy: OrderBy?
82 | var offset: Int
83 | var limit: Int
84 |
85 | init(login: String, orderBy: OrderBy? = nil, offset: Int = 0, limit: Int = 20) {
86 | self.login = login
87 | self.orderBy = orderBy
88 | self.offset = offset
89 | self.limit = limit
90 | }
91 |
92 | let baseURL = RubyChinaV3.BaseURL
93 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/topics" }
94 | var parameters: [String: AnyObject] {
95 | var parameters = [String: AnyObject]()
96 |
97 | if let orderBy = self.orderBy {
98 | parameters["order"] = orderBy.rawValue
99 | }
100 |
101 | parameters["limit"] = self.limit
102 | parameters["offset"] = self.offset
103 |
104 | return parameters
105 | }
106 |
107 | typealias DeserializedType = [Topic]
108 |
109 | func parseResponse(json: JSON) -> DeserializedType? {
110 | return DeserializedType(byJSON: json["topics"])
111 | }
112 | }
113 |
114 | struct Replies: EndpointType, OffsetPaginatable {
115 | enum OrderBy: String {
116 | case Recent = "recent"
117 | }
118 |
119 | let login: String
120 | let orderBy: OrderBy?
121 | var offset: Int
122 | var limit: Int
123 |
124 | init(login: String, orderBy: OrderBy? = nil, offset: Int = 0, limit: Int = 20) {
125 | self.login = login
126 | self.orderBy = orderBy
127 | self.offset = offset
128 | self.limit = limit
129 | }
130 |
131 | let baseURL = RubyChinaV3.BaseURL
132 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/replies" }
133 | var parameters: [String: AnyObject] {
134 | var parameters = [String: AnyObject]()
135 |
136 | if let orderBy = self.orderBy {
137 | parameters["order"] = orderBy.rawValue
138 | }
139 |
140 | parameters["limit"] = self.limit
141 | parameters["offset"] = self.offset
142 |
143 | return parameters
144 | }
145 |
146 | typealias DeserializedType = [Reply]
147 |
148 | func parseResponse(json: JSON) -> DeserializedType? {
149 | return DeserializedType(byJSON: json["replies"])
150 | }
151 | }
152 |
153 | struct Favorites: EndpointType, OffsetPaginatable {
154 | let login: String
155 | var offset: Int
156 | var limit: Int
157 |
158 | init(login: String, offset: Int = 0, limit: Int = 20) {
159 | self.login = login
160 | self.offset = offset
161 | self.limit = limit
162 | }
163 |
164 | let baseURL = RubyChinaV3.BaseURL
165 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/favorites" }
166 | var parameters: [String: AnyObject] {
167 | var parameters = [String: AnyObject]()
168 |
169 | parameters["limit"] = self.limit
170 | parameters["offset"] = self.offset
171 |
172 | return parameters
173 | }
174 |
175 | typealias DeserializedType = [Topic]
176 |
177 | func parseResponse(json: JSON) -> DeserializedType? {
178 | return DeserializedType(byJSON: json["topics"])
179 | }
180 | }
181 |
182 | struct Following: EndpointType, OffsetPaginatable {
183 | let login: String
184 | var offset: Int
185 | var limit: Int
186 |
187 | init(login: String, offset: Int = 0, limit: Int = 20) {
188 | self.login = login
189 | self.offset = offset
190 | self.limit = limit
191 | }
192 |
193 | let baseURL = RubyChinaV3.BaseURL
194 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/following" }
195 | var parameters: [String: AnyObject] {
196 | var parameters = [String: AnyObject]()
197 |
198 | parameters["limit"] = self.limit
199 | parameters["offset"] = self.offset
200 |
201 | return parameters
202 | }
203 |
204 | typealias DeserializedType = [User]
205 |
206 | func parseResponse(json: JSON) -> DeserializedType? {
207 | return DeserializedType(byJSON: json["users"])
208 | }
209 | }
210 |
211 | struct Followers: EndpointType, OffsetPaginatable {
212 | let login: String
213 | var offset: Int
214 | var limit: Int
215 |
216 | init(login: String, offset: Int = 0, limit: Int = 20) {
217 | self.login = login
218 | self.offset = offset
219 | self.limit = limit
220 | }
221 |
222 | let baseURL = RubyChinaV3.BaseURL
223 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/followers" }
224 | var parameters: [String: AnyObject] {
225 | var parameters = [String: AnyObject]()
226 |
227 | parameters["limit"] = self.limit
228 | parameters["offset"] = self.offset
229 |
230 | return parameters
231 | }
232 |
233 | typealias DeserializedType = [User]
234 |
235 | func parseResponse(json: JSON) -> DeserializedType? {
236 | return DeserializedType(byJSON: json["users"])
237 | }
238 | }
239 |
240 | struct Blocked: EndpointType, OffsetPaginatable {
241 | let login: String
242 | var offset: Int
243 | var limit: Int
244 |
245 | init(login: String, offset: Int = 0, limit: Int = 20) {
246 | self.login = login
247 | self.offset = offset
248 | self.limit = limit
249 | }
250 |
251 | let baseURL = RubyChinaV3.BaseURL
252 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/blocked" }
253 | var parameters: [String: AnyObject] {
254 | var parameters = [String: AnyObject]()
255 |
256 | parameters["limit"] = self.limit
257 | parameters["offset"] = self.offset
258 |
259 | return parameters
260 | }
261 |
262 | typealias DeserializedType = [User]
263 |
264 | func parseResponse(json: JSON) -> DeserializedType? {
265 | return DeserializedType(byJSON: json["users"])
266 | }
267 | }
268 |
269 | struct Follow: EndpointType {
270 | let login: String
271 |
272 | init(login: String) {
273 | self.login = login
274 | }
275 |
276 | let baseURL = RubyChinaV3.BaseURL
277 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/follow" }
278 | let method = HTTPMethod.POST
279 |
280 | typealias DeserializedType = AnyObject
281 | }
282 |
283 | struct UnFollow: EndpointType {
284 | let login: String
285 |
286 | init(login: String) {
287 | self.login = login
288 | }
289 |
290 | let baseURL = RubyChinaV3.BaseURL
291 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/unfollow" }
292 | let method = HTTPMethod.POST
293 |
294 | typealias DeserializedType = AnyObject
295 | }
296 |
297 | struct Block: EndpointType {
298 | let login: String
299 |
300 | init(login: String) {
301 | self.login = login
302 | }
303 |
304 | let baseURL = RubyChinaV3.BaseURL
305 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/block" }
306 | let method = HTTPMethod.POST
307 |
308 | typealias DeserializedType = AnyObject
309 | }
310 |
311 | struct UnBlock: EndpointType {
312 | let login: String
313 |
314 | init(login: String) {
315 | self.login = login
316 | }
317 |
318 | let baseURL = RubyChinaV3.BaseURL
319 | var path: String { return "\(RubyChinaV3.Users.Path)/\(self.login)/unblock" }
320 | let method = HTTPMethod.POST
321 |
322 | typealias DeserializedType = AnyObject
323 | }
324 | }
325 |
--------------------------------------------------------------------------------
/RubyChinaApp/Views/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
43 |
49 |
66 |
76 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
209 |
215 |
222 |
229 |
236 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
--------------------------------------------------------------------------------
/RubyChinaApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 47;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 231DF0C9E04F39E6FB98AE0F /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF5EE10C04C7D61695AC7 /* User.swift */; };
11 | 231DF13DA4A78C3CC1937377 /* Response+mapSwiftyJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DFD0EA6F48001E6B4E49D /* Response+mapSwiftyJSON.swift */; };
12 | 231DF20B184C8E0C4667DE14 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DFCD7A04A2B6FE984C601 /* Helpers.swift */; };
13 | 231DF4CFB128DFACB9AC4F66 /* CustomDebugStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF3CC95CBAEB48744C125 /* CustomDebugStringConvertible.swift */; };
14 | 231DF4EC0611FFB817390AA3 /* Ability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF69A0BA185B9D027ED3E /* Ability.swift */; };
15 | 231DF50026BAC93B41D61EDB /* OffsetPager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DFC00AE0E335A138F5A53 /* OffsetPager.swift */; };
16 | 231DF99B1F997294A173AFA0 /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DFE55B4688FA7D3E8B7D3 /* ViewModelType.swift */; };
17 | 231DF9A2818FD266CF1F71D5 /* TopicCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DFF604B049AB9648F4071 /* TopicCellViewModel.swift */; };
18 | 231DF9BC5E5C962FB01CA017 /* GlobalConstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF7E28829ED774FE9F9E3 /* GlobalConstant.swift */; };
19 | 231DF9D3F6E134E3A9DE4041 /* ModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF3FB0007F49DFD891DA9 /* ModelType.swift */; };
20 | 231DFA1A19C5011B9B4679CF /* Topic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF1962EC2499EB3374649 /* Topic.swift */; };
21 | 231DFC1935D67575003229D5 /* FirstViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF936033E22F103D80F2D /* FirstViewController.swift */; };
22 | 231DFC6CC70D378E5AEA3D54 /* Topics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF1591B7761CA1D5664FA /* Topics.swift */; };
23 | 231DFD9928EE58ECE1136122 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DFA0DD99841201D825416 /* String.swift */; };
24 | 231DFE21DA482EB20732B290 /* SwiftyJSONMappable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DF0253D5F58432452BC0D /* SwiftyJSONMappable.swift */; };
25 | 231DFFBAD45A20DD4A0ED9B5 /* SegueHandlerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DFE73E05968C2ED097E99 /* SegueHandlerType.swift */; };
26 | 31A9D2230E0FA063CB10C95D /* RubyChinaV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D1F987FAF91B5F76E086 /* RubyChinaV3.swift */; };
27 | 31A9D2CBCB1F99B39CA623B2 /* TopicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9DAC8958D8040CAEA9E5C /* TopicsViewController.swift */; };
28 | 31A9D309751958C2492EBBE1 /* SharedObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9DAB9935B984E0866C189 /* SharedObjects.swift */; };
29 | 31A9D3B9B3CFBB19EA43E81A /* UserDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9DB23F9DF37F7A239DF24 /* UserDetailViewController.swift */; };
30 | 31A9D53BF888A4CC55B73909 /* Photos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D0D14B6FD042B6FF04A5 /* Photos.swift */; };
31 | 31A9D73B6A82B2C1A35DFE0C /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D702F03DD6CC2106C2F4 /* APIError.swift */; };
32 | 31A9D8B39216AC35E7F70468 /* Reply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D6E8620E5FF3193037DA /* Reply.swift */; };
33 | 31A9D906AD995A2EDD558B0D /* Hello.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D12781FB0997905874A1 /* Hello.swift */; };
34 | 31A9D93EDFCE02895C35F011 /* NetworkLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9DC04E21F53880B6828FC /* NetworkLogger.swift */; };
35 | 31A9DA64933FE3C419778104 /* Users.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D82BB3D48D7D4FA16955 /* Users.swift */; };
36 | 31A9DB18DADE7BCD8C1F703B /* EndpointType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9DEE22D18AB275DE61316 /* EndpointType.swift */; };
37 | 31A9DB46CE52F141B02A2BFE /* TopicsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9DE25708DF430723AF16E /* TopicsTableViewCell.swift */; };
38 | 31A9DBE9CDA353194E87F9FC /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D9BB6354DAC96C2A8391 /* Node.swift */; };
39 | 31A9DC4ADAA4003191EF16B4 /* Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D525F4F40028ECB3130E /* Provider.swift */; };
40 | 31A9DCBACE014A3E33924156 /* APIResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9DE22F7AC1350AE0E54C4 /* APIResult.swift */; };
41 | 31A9DD18A3DE831B4DD37DB5 /* Replies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D89D2087401239B75AD4 /* Replies.swift */; };
42 | 31A9DD29918C765715235B96 /* TopicsPager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D691B9045DA938B842CE /* TopicsPager.swift */; };
43 | 31A9DD3637254632DF3BB3E9 /* TopicDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D5A1BC8CC0A7A61603AB /* TopicDetailViewController.swift */; };
44 | 31A9DE1B1C7112CD3FA27BD2 /* Nodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9DC00F1E6779F99683172 /* Nodes.swift */; };
45 | 31A9DFFFC5086527834E954B /* UIEmbedsInCellButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A9D3BEB756C5367744674F /* UIEmbedsInCellButton.swift */; };
46 | 777FF0F8E301B65C158228E4 /* Pods_RubyChinaApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00865D4B0C7BE8D9400D6533 /* Pods_RubyChinaApp.framework */; };
47 | C441C03E1C1468180035AD6E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441C03D1C1468180035AD6E /* AppDelegate.swift */; };
48 | C441C0471C1468180035AD6E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C441C0461C1468180035AD6E /* Assets.xcassets */; };
49 | C441C0551C1468180035AD6E /* RubyChinaAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441C0541C1468180035AD6E /* RubyChinaAppTests.swift */; };
50 | C441C0601C1468180035AD6E /* RubyChinaAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C441C05F1C1468180035AD6E /* RubyChinaAppUITests.swift */; };
51 | C45CBF6D1C2AD733001B6400 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C45CBF6C1C2AD733001B6400 /* Main.storyboard */; };
52 | C4C009131C1ECEEA003226BC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4C009111C1ECEEA003226BC /* LaunchScreen.storyboard */; };
53 | /* End PBXBuildFile section */
54 |
55 | /* Begin PBXContainerItemProxy section */
56 | C441C0511C1468180035AD6E /* PBXContainerItemProxy */ = {
57 | isa = PBXContainerItemProxy;
58 | containerPortal = C441C0321C1468180035AD6E /* Project object */;
59 | proxyType = 1;
60 | remoteGlobalIDString = C441C0391C1468180035AD6E;
61 | remoteInfo = RubyChinaApp;
62 | };
63 | C441C05C1C1468180035AD6E /* PBXContainerItemProxy */ = {
64 | isa = PBXContainerItemProxy;
65 | containerPortal = C441C0321C1468180035AD6E /* Project object */;
66 | proxyType = 1;
67 | remoteGlobalIDString = C441C0391C1468180035AD6E;
68 | remoteInfo = RubyChinaApp;
69 | };
70 | /* End PBXContainerItemProxy section */
71 |
72 | /* Begin PBXFileReference section */
73 | 00865D4B0C7BE8D9400D6533 /* Pods_RubyChinaApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RubyChinaApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74 | 0DF50A1A491FC939DAFA7FAD /* Pods-RubyChinaApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RubyChinaApp.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RubyChinaApp/Pods-RubyChinaApp.debug.xcconfig"; sourceTree = ""; };
75 | 231DF0253D5F58432452BC0D /* SwiftyJSONMappable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyJSONMappable.swift; sourceTree = ""; };
76 | 231DF1591B7761CA1D5664FA /* Topics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Topics.swift; sourceTree = ""; };
77 | 231DF1962EC2499EB3374649 /* Topic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Topic.swift; sourceTree = ""; };
78 | 231DF3CC95CBAEB48744C125 /* CustomDebugStringConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomDebugStringConvertible.swift; sourceTree = ""; };
79 | 231DF3FB0007F49DFD891DA9 /* ModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelType.swift; sourceTree = ""; };
80 | 231DF5EE10C04C7D61695AC7 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
81 | 231DF69A0BA185B9D027ED3E /* Ability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ability.swift; sourceTree = ""; };
82 | 231DF7E28829ED774FE9F9E3 /* GlobalConstant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalConstant.swift; sourceTree = ""; };
83 | 231DF936033E22F103D80F2D /* FirstViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstViewController.swift; sourceTree = ""; };
84 | 231DFA0DD99841201D825416 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; };
85 | 231DFC00AE0E335A138F5A53 /* OffsetPager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OffsetPager.swift; sourceTree = ""; };
86 | 231DFCD7A04A2B6FE984C601 /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; };
87 | 231DFD0EA6F48001E6B4E49D /* Response+mapSwiftyJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Response+mapSwiftyJSON.swift"; sourceTree = ""; };
88 | 231DFE55B4688FA7D3E8B7D3 /* ViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = ""; };
89 | 231DFE73E05968C2ED097E99 /* SegueHandlerType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegueHandlerType.swift; sourceTree = ""; };
90 | 231DFF604B049AB9648F4071 /* TopicCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopicCellViewModel.swift; sourceTree = ""; };
91 | 31A9D0D14B6FD042B6FF04A5 /* Photos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photos.swift; sourceTree = ""; };
92 | 31A9D12781FB0997905874A1 /* Hello.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hello.swift; sourceTree = ""; };
93 | 31A9D1F987FAF91B5F76E086 /* RubyChinaV3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RubyChinaV3.swift; sourceTree = ""; };
94 | 31A9D3BEB756C5367744674F /* UIEmbedsInCellButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIEmbedsInCellButton.swift; sourceTree = ""; };
95 | 31A9D525F4F40028ECB3130E /* Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Provider.swift; sourceTree = ""; };
96 | 31A9D5A1BC8CC0A7A61603AB /* TopicDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopicDetailViewController.swift; sourceTree = ""; };
97 | 31A9D691B9045DA938B842CE /* TopicsPager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopicsPager.swift; sourceTree = ""; };
98 | 31A9D6E8620E5FF3193037DA /* Reply.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reply.swift; sourceTree = ""; };
99 | 31A9D702F03DD6CC2106C2F4 /* APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; };
100 | 31A9D82BB3D48D7D4FA16955 /* Users.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Users.swift; sourceTree = ""; };
101 | 31A9D89D2087401239B75AD4 /* Replies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Replies.swift; sourceTree = ""; };
102 | 31A9D9BB6354DAC96C2A8391 /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; };
103 | 31A9DAB9935B984E0866C189 /* SharedObjects.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedObjects.swift; sourceTree = ""; };
104 | 31A9DAC8958D8040CAEA9E5C /* TopicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopicsViewController.swift; sourceTree = ""; };
105 | 31A9DB23F9DF37F7A239DF24 /* UserDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDetailViewController.swift; sourceTree = ""; };
106 | 31A9DC00F1E6779F99683172 /* Nodes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Nodes.swift; sourceTree = ""; };
107 | 31A9DC04E21F53880B6828FC /* NetworkLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkLogger.swift; sourceTree = ""; };
108 | 31A9DE22F7AC1350AE0E54C4 /* APIResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIResult.swift; sourceTree = ""; };
109 | 31A9DE25708DF430723AF16E /* TopicsTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopicsTableViewCell.swift; sourceTree = ""; };
110 | 31A9DEE22D18AB275DE61316 /* EndpointType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndpointType.swift; sourceTree = ""; };
111 | 5A7227A6FE7BF2CCEDCDF764 /* Pods-RubyChinaApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RubyChinaApp.release.xcconfig"; path = "Pods/Target Support Files/Pods-RubyChinaApp/Pods-RubyChinaApp.release.xcconfig"; sourceTree = ""; };
112 | C441C03A1C1468180035AD6E /* RubyChinaApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RubyChinaApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
113 | C441C03D1C1468180035AD6E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
114 | C441C0461C1468180035AD6E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
115 | C441C04B1C1468180035AD6E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
116 | C441C0501C1468180035AD6E /* RubyChinaAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RubyChinaAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
117 | C441C0541C1468180035AD6E /* RubyChinaAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RubyChinaAppTests.swift; sourceTree = ""; };
118 | C441C0561C1468180035AD6E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
119 | C441C05B1C1468180035AD6E /* RubyChinaAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RubyChinaAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
120 | C441C05F1C1468180035AD6E /* RubyChinaAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RubyChinaAppUITests.swift; sourceTree = ""; };
121 | C441C0611C1468180035AD6E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
122 | C45CBF6C1C2AD733001B6400 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; };
123 | C4BBE17C1C1DFDA00029C7C4 /* NetworkAbstraction.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkAbstraction.framework; path = "NetworkAbstraction/NetworkAbstraction/build/Debug-iphoneos/NetworkAbstraction.framework"; sourceTree = ""; };
124 | C4C009111C1ECEEA003226BC /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; };
125 | /* End PBXFileReference section */
126 |
127 | /* Begin PBXFrameworksBuildPhase section */
128 | C441C0371C1468180035AD6E /* Frameworks */ = {
129 | isa = PBXFrameworksBuildPhase;
130 | buildActionMask = 2147483647;
131 | files = (
132 | 777FF0F8E301B65C158228E4 /* Pods_RubyChinaApp.framework in Frameworks */,
133 | );
134 | runOnlyForDeploymentPostprocessing = 0;
135 | };
136 | C441C04D1C1468180035AD6E /* Frameworks */ = {
137 | isa = PBXFrameworksBuildPhase;
138 | buildActionMask = 2147483647;
139 | files = (
140 | );
141 | runOnlyForDeploymentPostprocessing = 0;
142 | };
143 | C441C0581C1468180035AD6E /* Frameworks */ = {
144 | isa = PBXFrameworksBuildPhase;
145 | buildActionMask = 2147483647;
146 | files = (
147 | );
148 | runOnlyForDeploymentPostprocessing = 0;
149 | };
150 | /* End PBXFrameworksBuildPhase section */
151 |
152 | /* Begin PBXGroup section */
153 | 231DF173354D34CB754E9E6B /* Models */ = {
154 | isa = PBXGroup;
155 | children = (
156 | 231DF69A0BA185B9D027ED3E /* Ability.swift */,
157 | 231DF1962EC2499EB3374649 /* Topic.swift */,
158 | 231DF5EE10C04C7D61695AC7 /* User.swift */,
159 | 31A9D6E8620E5FF3193037DA /* Reply.swift */,
160 | 31A9D9BB6354DAC96C2A8391 /* Node.swift */,
161 | );
162 | path = Models;
163 | sourceTree = "";
164 | };
165 | 231DF46BD9FF2D35A668DFC7 /* Libraries */ = {
166 | isa = PBXGroup;
167 | children = (
168 | 231DFCD7A04A2B6FE984C601 /* Helpers.swift */,
169 | 231DFD64D08DB90BA60E47AF /* Extensions */,
170 | 231DF9785D40E9D6A86952F8 /* Endpoints */,
171 | 231DF0253D5F58432452BC0D /* SwiftyJSONMappable.swift */,
172 | 31A9DC04E21F53880B6828FC /* NetworkLogger.swift */,
173 | 31A9DEE22D18AB275DE61316 /* EndpointType.swift */,
174 | 31A9DE22F7AC1350AE0E54C4 /* APIResult.swift */,
175 | 31A9D525F4F40028ECB3130E /* Provider.swift */,
176 | 31A9D702F03DD6CC2106C2F4 /* APIError.swift */,
177 | 31A9DD883B282D8FC2A94423 /* Pagers */,
178 | 231DF3FB0007F49DFD891DA9 /* ModelType.swift */,
179 | 231DFE55B4688FA7D3E8B7D3 /* ViewModelType.swift */,
180 | 231DFC00AE0E335A138F5A53 /* OffsetPager.swift */,
181 | 31A9D3BEB756C5367744674F /* UIEmbedsInCellButton.swift */,
182 | 231DFE73E05968C2ED097E99 /* SegueHandlerType.swift */,
183 | );
184 | path = Libraries;
185 | sourceTree = "";
186 | };
187 | 231DF500ECB4E1A69FDBC94B /* Controllers */ = {
188 | isa = PBXGroup;
189 | children = (
190 | 231DF936033E22F103D80F2D /* FirstViewController.swift */,
191 | 31A9D9A95AD53C33A9986EF6 /* Topics */,
192 | 31A9DDB7DDD3197B646D689C /* TopicDetail */,
193 | 31A9DF293219D76D767732C1 /* UserDetail */,
194 | );
195 | path = Controllers;
196 | sourceTree = "";
197 | };
198 | 231DF9785D40E9D6A86952F8 /* Endpoints */ = {
199 | isa = PBXGroup;
200 | children = (
201 | 231DF1591B7761CA1D5664FA /* Topics.swift */,
202 | 31A9D12781FB0997905874A1 /* Hello.swift */,
203 | 31A9D89D2087401239B75AD4 /* Replies.swift */,
204 | 31A9D82BB3D48D7D4FA16955 /* Users.swift */,
205 | 31A9DC00F1E6779F99683172 /* Nodes.swift */,
206 | 31A9D0D14B6FD042B6FF04A5 /* Photos.swift */,
207 | 31A9D1F987FAF91B5F76E086 /* RubyChinaV3.swift */,
208 | );
209 | path = Endpoints;
210 | sourceTree = "";
211 | };
212 | 231DFD64D08DB90BA60E47AF /* Extensions */ = {
213 | isa = PBXGroup;
214 | children = (
215 | 231DFA0DD99841201D825416 /* String.swift */,
216 | 231DFD0EA6F48001E6B4E49D /* Response+mapSwiftyJSON.swift */,
217 | 231DF3CC95CBAEB48744C125 /* CustomDebugStringConvertible.swift */,
218 | );
219 | path = Extensions;
220 | sourceTree = "";
221 | };
222 | 231DFED5EF52071DF32EA780 /* Views */ = {
223 | isa = PBXGroup;
224 | children = (
225 | C45CBF6C1C2AD733001B6400 /* Main.storyboard */,
226 | C4C009111C1ECEEA003226BC /* LaunchScreen.storyboard */,
227 | );
228 | path = Views;
229 | sourceTree = "";
230 | };
231 | 31A9D9A95AD53C33A9986EF6 /* Topics */ = {
232 | isa = PBXGroup;
233 | children = (
234 | 31A9DAC8958D8040CAEA9E5C /* TopicsViewController.swift */,
235 | 31A9DE25708DF430723AF16E /* TopicsTableViewCell.swift */,
236 | 231DFF604B049AB9648F4071 /* TopicCellViewModel.swift */,
237 | );
238 | path = Topics;
239 | sourceTree = "";
240 | };
241 | 31A9DD883B282D8FC2A94423 /* Pagers */ = {
242 | isa = PBXGroup;
243 | children = (
244 | 31A9D691B9045DA938B842CE /* TopicsPager.swift */,
245 | );
246 | path = Pagers;
247 | sourceTree = "";
248 | };
249 | 31A9DDB7DDD3197B646D689C /* TopicDetail */ = {
250 | isa = PBXGroup;
251 | children = (
252 | 31A9D5A1BC8CC0A7A61603AB /* TopicDetailViewController.swift */,
253 | );
254 | path = TopicDetail;
255 | sourceTree = "";
256 | };
257 | 31A9DF293219D76D767732C1 /* UserDetail */ = {
258 | isa = PBXGroup;
259 | children = (
260 | 31A9DB23F9DF37F7A239DF24 /* UserDetailViewController.swift */,
261 | );
262 | path = UserDetail;
263 | sourceTree = "";
264 | };
265 | 63022DAA76E0FC8C0F81604B /* Pods */ = {
266 | isa = PBXGroup;
267 | children = (
268 | 0DF50A1A491FC939DAFA7FAD /* Pods-RubyChinaApp.debug.xcconfig */,
269 | 5A7227A6FE7BF2CCEDCDF764 /* Pods-RubyChinaApp.release.xcconfig */,
270 | );
271 | name = Pods;
272 | sourceTree = "";
273 | };
274 | C441C0311C1468180035AD6E = {
275 | isa = PBXGroup;
276 | children = (
277 | C441C03C1C1468180035AD6E /* RubyChinaApp */,
278 | C441C0531C1468180035AD6E /* RubyChinaAppTests */,
279 | C441C05E1C1468180035AD6E /* RubyChinaAppUITests */,
280 | C441C03B1C1468180035AD6E /* Products */,
281 | F567E92F272F96D915869D42 /* Frameworks */,
282 | 63022DAA76E0FC8C0F81604B /* Pods */,
283 | );
284 | indentWidth = 4;
285 | sourceTree = "";
286 | tabWidth = 4;
287 | usesTabs = 0;
288 | };
289 | C441C03B1C1468180035AD6E /* Products */ = {
290 | isa = PBXGroup;
291 | children = (
292 | C441C03A1C1468180035AD6E /* RubyChinaApp.app */,
293 | C441C0501C1468180035AD6E /* RubyChinaAppTests.xctest */,
294 | C441C05B1C1468180035AD6E /* RubyChinaAppUITests.xctest */,
295 | );
296 | name = Products;
297 | sourceTree = "";
298 | };
299 | C441C03C1C1468180035AD6E /* RubyChinaApp */ = {
300 | isa = PBXGroup;
301 | children = (
302 | C441C03D1C1468180035AD6E /* AppDelegate.swift */,
303 | C441C0461C1468180035AD6E /* Assets.xcassets */,
304 | C441C04B1C1468180035AD6E /* Info.plist */,
305 | 231DF173354D34CB754E9E6B /* Models */,
306 | 231DF46BD9FF2D35A668DFC7 /* Libraries */,
307 | 231DF7E28829ED774FE9F9E3 /* GlobalConstant.swift */,
308 | 231DF500ECB4E1A69FDBC94B /* Controllers */,
309 | 231DFED5EF52071DF32EA780 /* Views */,
310 | 31A9DAB9935B984E0866C189 /* SharedObjects.swift */,
311 | );
312 | path = RubyChinaApp;
313 | sourceTree = "";
314 | };
315 | C441C0531C1468180035AD6E /* RubyChinaAppTests */ = {
316 | isa = PBXGroup;
317 | children = (
318 | C441C0541C1468180035AD6E /* RubyChinaAppTests.swift */,
319 | C441C0561C1468180035AD6E /* Info.plist */,
320 | );
321 | path = RubyChinaAppTests;
322 | sourceTree = "";
323 | };
324 | C441C05E1C1468180035AD6E /* RubyChinaAppUITests */ = {
325 | isa = PBXGroup;
326 | children = (
327 | C441C05F1C1468180035AD6E /* RubyChinaAppUITests.swift */,
328 | C441C0611C1468180035AD6E /* Info.plist */,
329 | );
330 | path = RubyChinaAppUITests;
331 | sourceTree = "";
332 | };
333 | F567E92F272F96D915869D42 /* Frameworks */ = {
334 | isa = PBXGroup;
335 | children = (
336 | C4BBE17C1C1DFDA00029C7C4 /* NetworkAbstraction.framework */,
337 | 00865D4B0C7BE8D9400D6533 /* Pods_RubyChinaApp.framework */,
338 | );
339 | name = Frameworks;
340 | sourceTree = "";
341 | };
342 | /* End PBXGroup section */
343 |
344 | /* Begin PBXNativeTarget section */
345 | C441C0391C1468180035AD6E /* RubyChinaApp */ = {
346 | isa = PBXNativeTarget;
347 | buildConfigurationList = C441C0641C1468180035AD6E /* Build configuration list for PBXNativeTarget "RubyChinaApp" */;
348 | buildPhases = (
349 | 76C29228BC822136BA74B5B8 /* Check Pods Manifest.lock */,
350 | C441C0361C1468180035AD6E /* Sources */,
351 | C441C0371C1468180035AD6E /* Frameworks */,
352 | C441C0381C1468180035AD6E /* Resources */,
353 | 723A93454DE10333FB22D762 /* Embed Pods Frameworks */,
354 | 673BF444D9301CF90A2FF44F /* Copy Pods Resources */,
355 | );
356 | buildRules = (
357 | );
358 | dependencies = (
359 | );
360 | name = RubyChinaApp;
361 | productName = RubyChinaApp;
362 | productReference = C441C03A1C1468180035AD6E /* RubyChinaApp.app */;
363 | productType = "com.apple.product-type.application";
364 | };
365 | C441C04F1C1468180035AD6E /* RubyChinaAppTests */ = {
366 | isa = PBXNativeTarget;
367 | buildConfigurationList = C441C0671C1468180035AD6E /* Build configuration list for PBXNativeTarget "RubyChinaAppTests" */;
368 | buildPhases = (
369 | C441C04C1C1468180035AD6E /* Sources */,
370 | C441C04D1C1468180035AD6E /* Frameworks */,
371 | C441C04E1C1468180035AD6E /* Resources */,
372 | );
373 | buildRules = (
374 | );
375 | dependencies = (
376 | C441C0521C1468180035AD6E /* PBXTargetDependency */,
377 | );
378 | name = RubyChinaAppTests;
379 | productName = RubyChinaAppTests;
380 | productReference = C441C0501C1468180035AD6E /* RubyChinaAppTests.xctest */;
381 | productType = "com.apple.product-type.bundle.unit-test";
382 | };
383 | C441C05A1C1468180035AD6E /* RubyChinaAppUITests */ = {
384 | isa = PBXNativeTarget;
385 | buildConfigurationList = C441C06A1C1468180035AD6E /* Build configuration list for PBXNativeTarget "RubyChinaAppUITests" */;
386 | buildPhases = (
387 | C441C0571C1468180035AD6E /* Sources */,
388 | C441C0581C1468180035AD6E /* Frameworks */,
389 | C441C0591C1468180035AD6E /* Resources */,
390 | );
391 | buildRules = (
392 | );
393 | dependencies = (
394 | C441C05D1C1468180035AD6E /* PBXTargetDependency */,
395 | );
396 | name = RubyChinaAppUITests;
397 | productName = RubyChinaAppUITests;
398 | productReference = C441C05B1C1468180035AD6E /* RubyChinaAppUITests.xctest */;
399 | productType = "com.apple.product-type.bundle.ui-testing";
400 | };
401 | /* End PBXNativeTarget section */
402 |
403 | /* Begin PBXProject section */
404 | C441C0321C1468180035AD6E /* Project object */ = {
405 | isa = PBXProject;
406 | attributes = {
407 | LastSwiftUpdateCheck = 0710;
408 | LastUpgradeCheck = 0710;
409 | ORGANIZATIONNAME = RubyChina;
410 | TargetAttributes = {
411 | C441C0391C1468180035AD6E = {
412 | CreatedOnToolsVersion = 7.1.1;
413 | };
414 | C441C04F1C1468180035AD6E = {
415 | CreatedOnToolsVersion = 7.1.1;
416 | TestTargetID = C441C0391C1468180035AD6E;
417 | };
418 | C441C05A1C1468180035AD6E = {
419 | CreatedOnToolsVersion = 7.1.1;
420 | TestTargetID = C441C0391C1468180035AD6E;
421 | };
422 | };
423 | };
424 | buildConfigurationList = C441C0351C1468180035AD6E /* Build configuration list for PBXProject "RubyChinaApp" */;
425 | compatibilityVersion = "Xcode 6.3";
426 | developmentRegion = English;
427 | hasScannedForEncodings = 0;
428 | knownRegions = (
429 | en,
430 | Base,
431 | );
432 | mainGroup = C441C0311C1468180035AD6E;
433 | productRefGroup = C441C03B1C1468180035AD6E /* Products */;
434 | projectDirPath = "";
435 | projectRoot = "";
436 | targets = (
437 | C441C0391C1468180035AD6E /* RubyChinaApp */,
438 | C441C04F1C1468180035AD6E /* RubyChinaAppTests */,
439 | C441C05A1C1468180035AD6E /* RubyChinaAppUITests */,
440 | );
441 | };
442 | /* End PBXProject section */
443 |
444 | /* Begin PBXResourcesBuildPhase section */
445 | C441C0381C1468180035AD6E /* Resources */ = {
446 | isa = PBXResourcesBuildPhase;
447 | buildActionMask = 2147483647;
448 | files = (
449 | C45CBF6D1C2AD733001B6400 /* Main.storyboard in Resources */,
450 | C441C0471C1468180035AD6E /* Assets.xcassets in Resources */,
451 | C4C009131C1ECEEA003226BC /* LaunchScreen.storyboard in Resources */,
452 | );
453 | runOnlyForDeploymentPostprocessing = 0;
454 | };
455 | C441C04E1C1468180035AD6E /* Resources */ = {
456 | isa = PBXResourcesBuildPhase;
457 | buildActionMask = 2147483647;
458 | files = (
459 | );
460 | runOnlyForDeploymentPostprocessing = 0;
461 | };
462 | C441C0591C1468180035AD6E /* Resources */ = {
463 | isa = PBXResourcesBuildPhase;
464 | buildActionMask = 2147483647;
465 | files = (
466 | );
467 | runOnlyForDeploymentPostprocessing = 0;
468 | };
469 | /* End PBXResourcesBuildPhase section */
470 |
471 | /* Begin PBXShellScriptBuildPhase section */
472 | 673BF444D9301CF90A2FF44F /* Copy Pods Resources */ = {
473 | isa = PBXShellScriptBuildPhase;
474 | buildActionMask = 2147483647;
475 | files = (
476 | );
477 | inputPaths = (
478 | );
479 | name = "Copy Pods Resources";
480 | outputPaths = (
481 | );
482 | runOnlyForDeploymentPostprocessing = 0;
483 | shellPath = /bin/sh;
484 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RubyChinaApp/Pods-RubyChinaApp-resources.sh\"\n";
485 | showEnvVarsInLog = 0;
486 | };
487 | 723A93454DE10333FB22D762 /* Embed Pods Frameworks */ = {
488 | isa = PBXShellScriptBuildPhase;
489 | buildActionMask = 2147483647;
490 | files = (
491 | );
492 | inputPaths = (
493 | );
494 | name = "Embed Pods Frameworks";
495 | outputPaths = (
496 | );
497 | runOnlyForDeploymentPostprocessing = 0;
498 | shellPath = /bin/sh;
499 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RubyChinaApp/Pods-RubyChinaApp-frameworks.sh\"\n";
500 | showEnvVarsInLog = 0;
501 | };
502 | 76C29228BC822136BA74B5B8 /* Check Pods Manifest.lock */ = {
503 | isa = PBXShellScriptBuildPhase;
504 | buildActionMask = 2147483647;
505 | files = (
506 | );
507 | inputPaths = (
508 | );
509 | name = "Check Pods Manifest.lock";
510 | outputPaths = (
511 | );
512 | runOnlyForDeploymentPostprocessing = 0;
513 | shellPath = /bin/sh;
514 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
515 | showEnvVarsInLog = 0;
516 | };
517 | /* End PBXShellScriptBuildPhase section */
518 |
519 | /* Begin PBXSourcesBuildPhase section */
520 | C441C0361C1468180035AD6E /* Sources */ = {
521 | isa = PBXSourcesBuildPhase;
522 | buildActionMask = 2147483647;
523 | files = (
524 | C441C03E1C1468180035AD6E /* AppDelegate.swift in Sources */,
525 | 231DF20B184C8E0C4667DE14 /* Helpers.swift in Sources */,
526 | 231DF9BC5E5C962FB01CA017 /* GlobalConstant.swift in Sources */,
527 | 231DF4EC0611FFB817390AA3 /* Ability.swift in Sources */,
528 | 231DFA1A19C5011B9B4679CF /* Topic.swift in Sources */,
529 | 231DF0C9E04F39E6FB98AE0F /* User.swift in Sources */,
530 | 231DFC1935D67575003229D5 /* FirstViewController.swift in Sources */,
531 | 231DFD9928EE58ECE1136122 /* String.swift in Sources */,
532 | 231DF13DA4A78C3CC1937377 /* Response+mapSwiftyJSON.swift in Sources */,
533 | 231DFE21DA482EB20732B290 /* SwiftyJSONMappable.swift in Sources */,
534 | 231DFC6CC70D378E5AEA3D54 /* Topics.swift in Sources */,
535 | 31A9D906AD995A2EDD558B0D /* Hello.swift in Sources */,
536 | 31A9D309751958C2492EBBE1 /* SharedObjects.swift in Sources */,
537 | 31A9D93EDFCE02895C35F011 /* NetworkLogger.swift in Sources */,
538 | 31A9D8B39216AC35E7F70468 /* Reply.swift in Sources */,
539 | 31A9DD18A3DE831B4DD37DB5 /* Replies.swift in Sources */,
540 | 31A9DA64933FE3C419778104 /* Users.swift in Sources */,
541 | 31A9DBE9CDA353194E87F9FC /* Node.swift in Sources */,
542 | 31A9DE1B1C7112CD3FA27BD2 /* Nodes.swift in Sources */,
543 | 31A9D53BF888A4CC55B73909 /* Photos.swift in Sources */,
544 | 31A9DB18DADE7BCD8C1F703B /* EndpointType.swift in Sources */,
545 | 31A9DCBACE014A3E33924156 /* APIResult.swift in Sources */,
546 | 31A9D2230E0FA063CB10C95D /* RubyChinaV3.swift in Sources */,
547 | 31A9DC4ADAA4003191EF16B4 /* Provider.swift in Sources */,
548 | 31A9D73B6A82B2C1A35DFE0C /* APIError.swift in Sources */,
549 | 31A9D2CBCB1F99B39CA623B2 /* TopicsViewController.swift in Sources */,
550 | 31A9DB46CE52F141B02A2BFE /* TopicsTableViewCell.swift in Sources */,
551 | 31A9DD29918C765715235B96 /* TopicsPager.swift in Sources */,
552 | 231DF9D3F6E134E3A9DE4041 /* ModelType.swift in Sources */,
553 | 231DF4CFB128DFACB9AC4F66 /* CustomDebugStringConvertible.swift in Sources */,
554 | 231DF9A2818FD266CF1F71D5 /* TopicCellViewModel.swift in Sources */,
555 | 231DF99B1F997294A173AFA0 /* ViewModelType.swift in Sources */,
556 | 231DF50026BAC93B41D61EDB /* OffsetPager.swift in Sources */,
557 | 31A9DD3637254632DF3BB3E9 /* TopicDetailViewController.swift in Sources */,
558 | 31A9DFFFC5086527834E954B /* UIEmbedsInCellButton.swift in Sources */,
559 | 31A9D3B9B3CFBB19EA43E81A /* UserDetailViewController.swift in Sources */,
560 | 231DFFBAD45A20DD4A0ED9B5 /* SegueHandlerType.swift in Sources */,
561 | );
562 | runOnlyForDeploymentPostprocessing = 0;
563 | };
564 | C441C04C1C1468180035AD6E /* Sources */ = {
565 | isa = PBXSourcesBuildPhase;
566 | buildActionMask = 2147483647;
567 | files = (
568 | C441C0551C1468180035AD6E /* RubyChinaAppTests.swift in Sources */,
569 | );
570 | runOnlyForDeploymentPostprocessing = 0;
571 | };
572 | C441C0571C1468180035AD6E /* Sources */ = {
573 | isa = PBXSourcesBuildPhase;
574 | buildActionMask = 2147483647;
575 | files = (
576 | C441C0601C1468180035AD6E /* RubyChinaAppUITests.swift in Sources */,
577 | );
578 | runOnlyForDeploymentPostprocessing = 0;
579 | };
580 | /* End PBXSourcesBuildPhase section */
581 |
582 | /* Begin PBXTargetDependency section */
583 | C441C0521C1468180035AD6E /* PBXTargetDependency */ = {
584 | isa = PBXTargetDependency;
585 | target = C441C0391C1468180035AD6E /* RubyChinaApp */;
586 | targetProxy = C441C0511C1468180035AD6E /* PBXContainerItemProxy */;
587 | };
588 | C441C05D1C1468180035AD6E /* PBXTargetDependency */ = {
589 | isa = PBXTargetDependency;
590 | target = C441C0391C1468180035AD6E /* RubyChinaApp */;
591 | targetProxy = C441C05C1C1468180035AD6E /* PBXContainerItemProxy */;
592 | };
593 | /* End PBXTargetDependency section */
594 |
595 | /* Begin XCBuildConfiguration section */
596 | C441C0621C1468180035AD6E /* Debug */ = {
597 | isa = XCBuildConfiguration;
598 | buildSettings = {
599 | ALWAYS_SEARCH_USER_PATHS = NO;
600 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
601 | CLANG_CXX_LIBRARY = "libc++";
602 | CLANG_ENABLE_MODULES = YES;
603 | CLANG_ENABLE_OBJC_ARC = YES;
604 | CLANG_WARN_BOOL_CONVERSION = YES;
605 | CLANG_WARN_CONSTANT_CONVERSION = YES;
606 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
607 | CLANG_WARN_EMPTY_BODY = YES;
608 | CLANG_WARN_ENUM_CONVERSION = YES;
609 | CLANG_WARN_INT_CONVERSION = YES;
610 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
611 | CLANG_WARN_UNREACHABLE_CODE = YES;
612 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
613 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
614 | COPY_PHASE_STRIP = NO;
615 | DEBUG_INFORMATION_FORMAT = dwarf;
616 | ENABLE_STRICT_OBJC_MSGSEND = YES;
617 | ENABLE_TESTABILITY = YES;
618 | GCC_C_LANGUAGE_STANDARD = gnu99;
619 | GCC_DYNAMIC_NO_PIC = NO;
620 | GCC_NO_COMMON_BLOCKS = YES;
621 | GCC_OPTIMIZATION_LEVEL = 0;
622 | GCC_PREPROCESSOR_DEFINITIONS = (
623 | "DEBUG=1",
624 | "$(inherited)",
625 | );
626 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
627 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
628 | GCC_WARN_UNDECLARED_SELECTOR = YES;
629 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
630 | GCC_WARN_UNUSED_FUNCTION = YES;
631 | GCC_WARN_UNUSED_VARIABLE = YES;
632 | IPHONEOS_DEPLOYMENT_TARGET = 9.1;
633 | MTL_ENABLE_DEBUG_INFO = YES;
634 | ONLY_ACTIVE_ARCH = YES;
635 | SDKROOT = iphoneos;
636 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
637 | };
638 | name = Debug;
639 | };
640 | C441C0631C1468180035AD6E /* Release */ = {
641 | isa = XCBuildConfiguration;
642 | buildSettings = {
643 | ALWAYS_SEARCH_USER_PATHS = NO;
644 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
645 | CLANG_CXX_LIBRARY = "libc++";
646 | CLANG_ENABLE_MODULES = YES;
647 | CLANG_ENABLE_OBJC_ARC = YES;
648 | CLANG_WARN_BOOL_CONVERSION = YES;
649 | CLANG_WARN_CONSTANT_CONVERSION = YES;
650 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
651 | CLANG_WARN_EMPTY_BODY = YES;
652 | CLANG_WARN_ENUM_CONVERSION = YES;
653 | CLANG_WARN_INT_CONVERSION = YES;
654 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
655 | CLANG_WARN_UNREACHABLE_CODE = YES;
656 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
657 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
658 | COPY_PHASE_STRIP = NO;
659 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
660 | ENABLE_NS_ASSERTIONS = NO;
661 | ENABLE_STRICT_OBJC_MSGSEND = YES;
662 | GCC_C_LANGUAGE_STANDARD = gnu99;
663 | GCC_NO_COMMON_BLOCKS = YES;
664 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
665 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
666 | GCC_WARN_UNDECLARED_SELECTOR = YES;
667 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
668 | GCC_WARN_UNUSED_FUNCTION = YES;
669 | GCC_WARN_UNUSED_VARIABLE = YES;
670 | IPHONEOS_DEPLOYMENT_TARGET = 9.1;
671 | MTL_ENABLE_DEBUG_INFO = NO;
672 | SDKROOT = iphoneos;
673 | VALIDATE_PRODUCT = YES;
674 | };
675 | name = Release;
676 | };
677 | C441C0651C1468180035AD6E /* Debug */ = {
678 | isa = XCBuildConfiguration;
679 | baseConfigurationReference = 0DF50A1A491FC939DAFA7FAD /* Pods-RubyChinaApp.debug.xcconfig */;
680 | buildSettings = {
681 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
682 | FRAMEWORK_SEARCH_PATHS = "$(inherited)";
683 | INFOPLIST_FILE = RubyChinaApp/Info.plist;
684 | IPHONEOS_DEPLOYMENT_TARGET = 9.1;
685 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
686 | "OTHER_SWIFT_FLAGS[arch=*]" = "$(inherited) \"-D\" \"COCOAPODS\" -v";
687 | PRODUCT_BUNDLE_IDENTIFIER = RubyChina.RubyChinaApp;
688 | PRODUCT_NAME = "$(TARGET_NAME)";
689 | };
690 | name = Debug;
691 | };
692 | C441C0661C1468180035AD6E /* Release */ = {
693 | isa = XCBuildConfiguration;
694 | baseConfigurationReference = 5A7227A6FE7BF2CCEDCDF764 /* Pods-RubyChinaApp.release.xcconfig */;
695 | buildSettings = {
696 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
697 | FRAMEWORK_SEARCH_PATHS = "$(inherited)";
698 | INFOPLIST_FILE = RubyChinaApp/Info.plist;
699 | IPHONEOS_DEPLOYMENT_TARGET = 9.1;
700 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
701 | PRODUCT_BUNDLE_IDENTIFIER = RubyChina.RubyChinaApp;
702 | PRODUCT_NAME = "$(TARGET_NAME)";
703 | };
704 | name = Release;
705 | };
706 | C441C0681C1468180035AD6E /* Debug */ = {
707 | isa = XCBuildConfiguration;
708 | buildSettings = {
709 | BUNDLE_LOADER = "$(TEST_HOST)";
710 | INFOPLIST_FILE = RubyChinaAppTests/Info.plist;
711 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
712 | PRODUCT_BUNDLE_IDENTIFIER = RubyChina.RubyChinaAppTests;
713 | PRODUCT_NAME = "$(TARGET_NAME)";
714 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RubyChinaApp.app/RubyChinaApp";
715 | };
716 | name = Debug;
717 | };
718 | C441C0691C1468180035AD6E /* Release */ = {
719 | isa = XCBuildConfiguration;
720 | buildSettings = {
721 | BUNDLE_LOADER = "$(TEST_HOST)";
722 | INFOPLIST_FILE = RubyChinaAppTests/Info.plist;
723 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
724 | PRODUCT_BUNDLE_IDENTIFIER = RubyChina.RubyChinaAppTests;
725 | PRODUCT_NAME = "$(TARGET_NAME)";
726 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RubyChinaApp.app/RubyChinaApp";
727 | };
728 | name = Release;
729 | };
730 | C441C06B1C1468180035AD6E /* Debug */ = {
731 | isa = XCBuildConfiguration;
732 | buildSettings = {
733 | INFOPLIST_FILE = RubyChinaAppUITests/Info.plist;
734 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
735 | PRODUCT_BUNDLE_IDENTIFIER = RubyChina.RubyChinaAppUITests;
736 | PRODUCT_NAME = "$(TARGET_NAME)";
737 | TEST_TARGET_NAME = RubyChinaApp;
738 | USES_XCTRUNNER = YES;
739 | };
740 | name = Debug;
741 | };
742 | C441C06C1C1468180035AD6E /* Release */ = {
743 | isa = XCBuildConfiguration;
744 | buildSettings = {
745 | INFOPLIST_FILE = RubyChinaAppUITests/Info.plist;
746 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
747 | PRODUCT_BUNDLE_IDENTIFIER = RubyChina.RubyChinaAppUITests;
748 | PRODUCT_NAME = "$(TARGET_NAME)";
749 | TEST_TARGET_NAME = RubyChinaApp;
750 | USES_XCTRUNNER = YES;
751 | };
752 | name = Release;
753 | };
754 | /* End XCBuildConfiguration section */
755 |
756 | /* Begin XCConfigurationList section */
757 | C441C0351C1468180035AD6E /* Build configuration list for PBXProject "RubyChinaApp" */ = {
758 | isa = XCConfigurationList;
759 | buildConfigurations = (
760 | C441C0621C1468180035AD6E /* Debug */,
761 | C441C0631C1468180035AD6E /* Release */,
762 | );
763 | defaultConfigurationIsVisible = 0;
764 | defaultConfigurationName = Release;
765 | };
766 | C441C0641C1468180035AD6E /* Build configuration list for PBXNativeTarget "RubyChinaApp" */ = {
767 | isa = XCConfigurationList;
768 | buildConfigurations = (
769 | C441C0651C1468180035AD6E /* Debug */,
770 | C441C0661C1468180035AD6E /* Release */,
771 | );
772 | defaultConfigurationIsVisible = 0;
773 | defaultConfigurationName = Release;
774 | };
775 | C441C0671C1468180035AD6E /* Build configuration list for PBXNativeTarget "RubyChinaAppTests" */ = {
776 | isa = XCConfigurationList;
777 | buildConfigurations = (
778 | C441C0681C1468180035AD6E /* Debug */,
779 | C441C0691C1468180035AD6E /* Release */,
780 | );
781 | defaultConfigurationIsVisible = 0;
782 | defaultConfigurationName = Release;
783 | };
784 | C441C06A1C1468180035AD6E /* Build configuration list for PBXNativeTarget "RubyChinaAppUITests" */ = {
785 | isa = XCConfigurationList;
786 | buildConfigurations = (
787 | C441C06B1C1468180035AD6E /* Debug */,
788 | C441C06C1C1468180035AD6E /* Release */,
789 | );
790 | defaultConfigurationIsVisible = 0;
791 | defaultConfigurationName = Release;
792 | };
793 | /* End XCConfigurationList section */
794 | };
795 | rootObject = C441C0321C1468180035AD6E /* Project object */;
796 | }
797 |
--------------------------------------------------------------------------------