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