Repository: orakaro/The-introduction-to-RxSwift-you-have-been-missing
Branch: master
Commit: 57fd9b242d3f
Files: 28
Total size: 78.5 KB
Directory structure:
gitextract_f2b1hkwg/
├── .gitignore
├── Podfile
├── README.md
├── WhoToFollow/
│ ├── AppDelegate.swift
│ ├── Assets.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ └── x.imageset/
│ │ └── Contents.json
│ ├── Base.lproj/
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Extension.swift
│ ├── FollowTableViewCell.swift
│ ├── FollowTableViewController.swift
│ ├── Github.swift
│ ├── Info.plist
│ ├── User.swift
│ └── UserModel.swift
├── WhoToFollow.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcuserdata/
│ │ └── DTVD.xcuserdatad/
│ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata/
│ ├── DTVD.xcuserdatad/
│ │ └── xcschemes/
│ │ ├── WhoToFollow.xcscheme
│ │ └── xcschememanagement.plist
│ └── vunhat_minh.xcuserdatad/
│ └── xcschemes/
│ ├── WhoToFollow.xcscheme
│ └── xcschememanagement.plist
├── WhoToFollow.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcuserdata/
│ └── vunhat_minh.xcuserdatad/
│ ├── UserInterfaceState.xcuserstate
│ └── xcdebugger/
│ └── Breakpoints_v2.xcbkptlist
└── WhoToFollowTests/
├── Info.plist
└── WhoToFollowTests.swift
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
Pods
WhoToFollow.xcworkspace/xcuserdata/DTVD.xcuserdatad/
.DS_Store
.swp
.swo
================================================
FILE: Podfile
================================================
# Uncomment this line to define a global platform for your project
platform :ios, '8.0'
# Uncomment this line if you're using Swift
use_frameworks!
target 'WhoToFollow' do
pod 'RxSwift', '~> 2.0'
pod 'RxCocoa', '~> 2.0'
pod 'Moya/RxSwift'
pod 'Moya-ModelMapper/RxSwift'
pod 'RxOptional'
pod 'SwiftyJSON', :git => 'https://github.com/SwiftyJSON/SwiftyJSON.git'
end
target 'WhoToFollowTests' do
pod 'Quick', '~> 0.8.0'
pod 'Nimble', '3.0.0'
pod 'RxBlocking', '~> 2.0'
pod 'RxTests', '~> 2.2'
end
================================================
FILE: README.md
================================================
# The introduction to RxSwift you've been missing
This work is inspired by [The introduction to Reactive Programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) from [@andrestaltz](https://twitter.com/andrestaltz). I recreated his RxJS sample code in RxSwift with a step-by-step walkthrough for those struggling with learning RxSwift due to lack of good references (as I did).
---
So you're finding yourself having trouble with learning this new Swift trend? You are not alone.
RxSwift is hard, especially with the lack of good references.
Every tutorial out there is either too general or too specific, and ReactiveX documents just don't help:
> Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
> Projects each element of an observable sequence into a new sequence of observable sequences
> by incorporating the element's index and then transforms an observable sequence of observable sequences
> into an observable sequence producing values only from the most recent observable sequence.

I ended up digging into RxSwift examples and some [open source apps](https://github.com/devxoul/RxTodo).
The RxSwift's [very first document](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Why.md)
brings in RxSwift's `Binding` or `Retry` - things that I haven't got a clue about.
Also, reading the code is not easy, since it introduces RxSwift in great detail with `RxDataSources` and `Moya/RxSwift` *at the same time*.
So I decided to code a sample app that presents exactly "Who to Follow" pattern with step-by-step explanations. This is equivalent to Andre's work, but is written in Swift instead and I hope this can help you learn RxSwift easier than me :smile:
# What is Reactive Programming?
Tapping a button, typing one character inside a text field, etc, every occurrence triggered by user can be considered as a typical asynchronous event.
What if our user repeatedly taps an element, or is continuously typing in a search bar?
This time we have *asynchronous event streams*.
```
--a---b-c---d---X---|->
a, b, c, d are events
X is an error event
| is the 'completed' signal
---> is the timeline
```
You are able to create data streams out of anything, not just from tap or typing events.
Streams are cheap and ubiquitous. Anything can be a stream: variables, user inputs, properties, caches, data structures, etc.
For example, imagine your Twitter feed would be a data stream in the same fashion that tap events are.
You can listen to that stream and react accordingly.
On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams.
That's where the "functional" magic kicks in.
A stream can be used as an input to another one.
Even multiple streams can be used as inputs to another stream.
You can merge two streams.
You can filter a stream to get another one that has only those events you are interested in.
You can map data values from one stream to another new one.
```
buttonTapStream: ---t----t--t----t------t-->
vvvvv map(t becomes 1) vvvv
---1----1--1----1------1-->
vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
```
In Reactive World streams are called **Observables**, represented by a timeline with ongoing events in chronological order.
Every Observable is **immutable**, which means that each stream composition will create a completely new Observable.
Reactive Programming(RP) introduced a whole new paradigm in development for reactive applications.
Mobile apps today are highly interactive with UI events related to the flow of data from the back-end.
No screen transitions are made but a user can see search results while typing in search bar, or pull down for instant refresh, etc.
# Implementing a "Who to follow" suggestions box
Let's dive into a real-world example. This is Twitter's UI element that suggests other accounts you may want to follow

I am going to the implement core features below
* On startup, load accounts data from the API and display 3 suggestions
* On tapping "Refresh", load 3 other account suggestions into the 3 rows
* On tapping 'x' button on an account row, clear only that current account and display another
* Each row displays the account's avatar and their name.
Because Twitter doesn't provide its API for unauthorized public use, I will use Github's API instead.
There's a [Github API](https://developer.github.com/v3/users/#get-all-users) for getting users with a `since` offset parameter.
You can check the working code by cloning this repo.
# Request and Response
Let's start with the easiest feature: "On startup, load accounts data from the API and display 3 suggestions". This is simply:
1. Doing a request
2. Getting response
3. Rendering response data to UITableView
Doing a request is the most basic part in this project.
We already know some great libraries for requests, such as Alamofire, but let's think in Rx first.
Consider that request's URL is a string, in this case `https://api.github.com/users`, then we can create our very first *Observable object*: `Observable`
```Swift
let requestStream: Observable = Observable.just("https://api.github.com/users")
```
This is a *stream* of URLs, in this case only one event (the URL string) will be emitted.
```
--a------|->
Where a is the string "https://api.github.com/users"
```
`requestStream` is just a stream of strings, it does nothing else. We need to make the "real" request happen when the event is emmited by *subscribing* to it
```Swift
requestStream.subscribeNext { url in
// Do the real request to Github API, get back a `User` model
let responseStream: Observable<[User]> = UserModel().findUsers(url)
}
```
Note that `responseStream` is also an `Observable`.
You can find the implementation details of `UserModel().findUsers(url)` later on in this repo, but for now just consider it as a method which returns a list of Users from the Github response, wrapped inside an `Observable` type.
So the next step is rendering this list of Users to UITableView, which can be done by subcribing to the `responseStream` again
```Swift
requestStream.subscribeNext { url in
let responseStream: Observable<[User]> = UserModel().findUsers(url)
responseStream.subscribeNext { users in
// ...
}
}
```
If you were quick to notice, we have one `subscribeNext` call inside another, which is somewhat akin to callback hell.
In Rx there are simple mechanisms for transforming and creating new streams out of others, and the corresponding method here is `map(f)`.
```Swift
let responseStream = requestStream.map { url in
return UserModel().findUsers(url)
}
```
We just created a beast called "metastream": a stream of streams.
Don't panic just yet.
A metastream is a stream where each emitted value is yet another stream.
You can think of it as [pointers](https://en.wikipedia.org/wiki/Pointer_(computer_programming)): each emitted value is a pointer to another stream.
In our example, each request URL is mapped to a pointer to the stream containing the corresponding response.

A metastream looks confusing and we just want a simple stream of responses where each emitted value is just a `[User]`, not stream of `[User]`.
Say hi to `flatMap(f)`, a version of map() that "flattens" a metastream by emitting on the "trunk" stream everything that will be emitted on "branch" streams.
`flatmap` is not a "fix" and metastreams are not a bug; these are really the tools for dealing with asynchronous responses in Rx.
```Swift
let responseStream = requestStream.flatMap { url in
return UserModel().findUsers(url)
}
```

Nice. If we have more events happenning in `requestStream` (like continuous tapping of a button or typing text), we will have the corresponding response results on `responseStream`, as expected:
```
requestStream: --url-------url----------url------------|->
responseStream: -----[User]-----[User]-----[User]-------|->
```
Joining all the code until now, we have:
```Swift
let requestStream: Observable = Observable.just("https://api.github.com/users")
let responseStream = requestStream.flatMap { url in
return UserModel().findUsers(url)
}
responseStream.subscribeNext { users in
// users is a normal [User] list, here comes the UI Rendering part
}
```
# The refresh button
We will want a set of 3 new users every time a user taps the "refresh" button. How do we achieve this scenario?
We need 2 streams: a stream of tap events on the refresh button, and a stream of API URLs transformed from that stream.
In RxSwift, the stream of tap events can be created with method `rx_tap`
```Swift
let refreshStream = refresh.rx_tap
let requestStream: Observable = refreshStream.map { _ in
let random = Array(1...1000).random()
return "https://api.github.com/users/" + String(random)
}
```
*`refresh` is an outlet for a Refresh button in our class, and random() is a custom extension*
Because I'm dumb and I don't have automated tests, I just broke one of our previously built features: a request doesn't happen anymore on startup, it happens only when the refresh button is tapped.
Urgh. I need both behaviors: a request when either the refresh button is tapped or the UITableVIew has just loaded.
We know how to make a separate streams for each one of those cases:
```Swift
let refreshStream = refresh.rx_tap
let requestStream: Observable = refreshStream.map { _ in
let random = Array(1...1000).random()
return "https://api.github.com/users/" + String(random)
}
let beginningStream: Observable = Observable.just("https://api.github.com/users")
```
But how can we "merge" these two into one? Well, there's `merge()`.
```
stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
vvvvvvvvv merge vvvvvvvvv
---a-B---C--e--D--o----->
```
In detail:
```Swift
let requestStream = Observable.of(refreshStream, beginningStream).merge()
```
And there is a cleaner way without the intermediate streams:
```Swift
let refreshStream = refresh.rx_tap.startWith(()) // Here
let requestStream: Observable = refreshStream.map { _ in
let random = Array(1...1000).random()
return "https://api.github.com/users/" + String(random)
}
```
The `startWith()` function does exactly what you think it does. No matter how your input stream looks like, the output stream resulting of `startWith(x)` will have x at the beginning. By adding `startWith()` to `refreshStream`, we "emulate" a refresh tap on startup.
# 3 suggestions streams
As soons as we received 'users' data from `responseStream`, we will want to show it immmediately on the 3 UITableVIewCells.
Let's think about Reactive mantra: "Everything is a stream"

So let's create a seperate stream *for each cell*.
```Swift
// Inside func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
let userStream: Observable = responseStream.map { users in
guard users.count > 0 else {return nil}
return users.random()
}
```
With the refresh button we have a problem: as soon as user taps 'Refresh', the current 3 suggestions are not cleared.
New suggestions come in only after a response has arrived, but to make the UI look nice, we need to clean out the current suggestions when refresh is tapped.
We can do that by mapping Refresh tap to a nil stream, and merge to above `userStream` as such:
```Swift
let nilOnRefreshTapStream: Observable = refresh.rx_tap
.map {_ in return nil}
let suggestionStream = Observable.of(userStream, nilOnRefreshTapStream)
.merge()
```
And when rendering, we interpret `nil` as "no data", hence hiding cell's UI element:
```Swift
suggestionStream.subscribeNext{ op in
guard let u = op else { return self.clearCell(cell) }
return self.setCell(cell, user: u )
}.addDisposableTo(cell.disposeBagCell)
```
The big picture is now:
```
refreshStream: ----------o--------o---->
requestStream: -r--------r--------r---->
responseStream: ----R---------R------R-->
suggestionStream(Cell 1): ----s-----N---s----N-s-->
suggestionStream(Cell 2): ----q-----N---q----N-q-->
suggestionStream(Cell 3): ----t-----N---t----N-t-->
```
Where N stands for nil.
As a bonus, we can also render "empty" suggestions on startup.
That is done by adding `.startWith(.None)` to the suggestion streams:
```Swift
let nilOnRefreshTapStream: Observable = refresh.rx_tap
.map {_ in return nil}
let suggestionStream = Observable.of(userStream, nilOnRefreshTapStream)
.merge()
.startWith(.None)
```
Which results in:
```
refreshStream: ----------o--------o---->
requestStream: -r--------r--------r---->
responseStream: ----R---------R------R-->
suggestionStream(Cell 1): -N--s-----N---s----N-s-->
suggestionStream(Cell 2): -N--q-----N---q----N-q-->
suggestionStream(Cell 3): -N--t-----N---t----N-t-->
```
# Closing a suggestion and using cached responses
There is one feature remaining to implement.
Each suggestion should have its own 'x' button for closing it, and loading another in its place.
At first thought, you could say it's enough to make a new request when any close button is tapped
```Swift
let closeStream = cell.cancel.rx_tap // "cancel" is outlet for cancel button
let requestStream = Observable.of(refreshStream, closeStream)
.merge()
.map { _ in
let random = Array(1...1000).random()
return "https://api.github.com/users/" + String(random)
}
```
This will close and reload *all suggestion*, rather than just only the one user tapped on.
There are a couple of different ways of solving this, and to keep it interesting, we will solve it by reusing previous responses.
The API's response page size is 100 users while we were using just 3 of those, so there is plenty of fresh data available.
No need to request more.
Again, let's think in streams. When a 'close' tap event happens, we want to use the most recently emitted (and cached) response on `responseStream` to get one random user from the list in the response. As such:
```
requestStream: --r--------------->
responseStream: ------R----------->
closeClickStream: ------------c----->
suggestionStream: ------s-----s----->
```
In Rx* there is a combinator function called `combineLatest(f)` that seems to do what we need.
It takes two streams A and B as inputs, and whenever either stream emits a value, combineLatest joins the two most recently emitted values a and b from both streams and outputs a value c = f(x,y), where f is a function you define.
It is better explained with a diagram:
```
stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
vvvvvvvv combineLatest(f) vvvvvvv
----AB---AC--EC---ED--ID--IQ---->
where f is the uppercase function
```
We can apply combineLatest() on `closeStream` and `responseStream`, so that whenever the close button is tapped, we get the latest response emitted and produce a new value on `suggestionStream`.
On the other hand, `combineLatest(f)` is symmetric: whenever a new response is emitted on `responseStream`, it will combine with the latest 'close' tap to produce a new suggestion.
```Swift
let closeStream = cell.cancel.rx_tap
let userStream: Observable = Observable.combineLatest(closeStream, responseStream)
{ (_, users) in
guard users.count > 0 else {return nil}
return users.random()
}
let nilOnRefreshTapStream: Observable = refresh.rx_tap.map {_ in return nil}
let suggestionStream = Observable.of(userStream, nilOnRefreshTapStream)
.merge()
.startWith(.None)
```
One piece is still missing in the puzzle.
The `combineLatest(f)` uses the most recent of the two sources, but if one of those sources hasn't emitted anything yet, `combineLatest(f)` cannot produce a data event on the output stream.
If you look at the ASCII diagram above, you will see that the output has nothing when the first stream emitted value a.
Only when the second stream emitted value b could it produce an output value.
There are different ways of solving this, and we will stay with the simplest one, which is simulating a tap to the 'close' button on startup:
```Swift
let closeStream = cell.cancel.rx_tap.startWith(())
```
# Wrapping up
We are done. The complete code is below
```Swift
let refreshStream = refresh.rx_tap.startWith(())
let requestStream: Observable = refreshStream.map { _ in
let random = Array(1...1000).random()
return "https://api.github.com/users/" + String(random)
}
let responseStream = requestStream.flatMap { url in
return UserModel().findUsers(url)
}
// Inside func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
let closeStream = cell.cancel.rx_tap.startWith(())
let userStream: Observable = Observable.combineLatest(closeStream, responseStream)
{ (_, users) in
guard users.count > 0 else {return nil}
return users.random()
}
let nilOnRefreshTapStream: Observable = refresh.rx_tap.map {_ in return nil}
let suggestionStream = Observable.of(userStream, nilOnRefreshTapStream)
.merge()
.startWith(.None)
suggestionStream.subscribeNext{ op in
guard let u = op else { return self.clearCell(cell) }
return self.setCell(cell, user: u )
}.addDisposableTo(cell.disposeBagCell)
```
You can see the working example in this repo.
This example is small but dense: it features management of multiple events with proper separation of concerns, and even caching of responses.
The functional style made the code look more declarative than imperative: we are not giving a sequence of instructions to execute, we are just telling what something is by defining relationships between streams.
For instance, with Rx we told the computer that `suggestionStream` is the `closeStream` combined with one user from the latest response, besides being nil when a refresh happens or program startup happened.
Notice also the impressive absence of control flow elements such as if, for, while, and the typical callback-based control flow that you expect from a Swift/IOS application.
You can even get rid of the if and else in the `subscribeNext()` above by using `filter()` if you want (I'll leave the implementation details to you as an exercise).
In Rx, we have stream functions such as `map`, `filter`, `scan`, `merge`, `combineLatest`, `startWith`, and many more to control the flow of an event-driven program.
This toolset of functions gives you more power in less code.
# Where to go from here
If you think RxSwift will be your preferred library for IOS Programming, take some time to get acquainted with [RxSwift API](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/API.md) for transforming, combining, and creating Observables.
If you want to understand those functions in diagrams of streams, take a look at [Marble diagrams](http://rxmarbles.com/).
Whenever you get stuck trying to do something, draw those diagrams, think about them, look at the long list of functions, and think more.
This workflow has been effective in my experience.
Once you start getting the hang of programming with RxSwift, you will need to get used to libraries which are using it such as `RxCocoa`, `Moya/RxSwift`, `RxDataSources` and then [Driver](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Units.md), etc.
Finally, sharpen your skills further by learning real functional programming, and getting acquainted with issues such as side effects that affect Rx.
If this tutorial helped you, [tweet it forward](https://twitter.com/intent/tweet?original_referer=https:%2F%2Fgithub.com%2FDTVD%2FThe-introduction-to-RxSwift-you-have-been-missing&text=The%20introduction%20to%20RxSwift%20you%27ve%20been%20missing&tw_p=tweetbutton&url=https:%2F%2Fgithub.com%2FDTVD%2FThe-introduction-to-RxSwift-you-have-been-missing&via=dtvd88).
### Legal
This is primarily created by Andre Cesar de Souza Medeiros (alias "Andre Staltz"), 2014, and modified by Vu Nhat Minh (@Orakaro), 2016.
"Introduction to RxSwift you've been missing" by Vu Nhat Minh is licensed under a [Creative Commons Attribution-NonCommercial 4.0 International License]("Introduction to Reactive Programming you've been missing" by Andre Staltz is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.).
================================================
FILE: WhoToFollow/AppDelegate.swift
================================================
//
// AppDelegate.swift
// WhoToFollow
//
// Created by DTVD on 7/19/16.
// Copyright © 2016 DTVD. All rights reserved.
//
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(application: UIApplication) {
// 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.
// 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.
}
func applicationDidEnterBackground(application: UIApplication) {
// 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.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(application: UIApplication) {
// 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.
}
func applicationDidBecomeActive(application: UIApplication) {
// 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.
}
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
================================================
FILE: WhoToFollow/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: WhoToFollow/Assets.xcassets/Contents.json
================================================
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: WhoToFollow/Assets.xcassets/x.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "multiply.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: WhoToFollow/Base.lproj/LaunchScreen.storyboard
================================================
================================================
FILE: WhoToFollow/Base.lproj/Main.storyboard
================================================
================================================
FILE: WhoToFollow/Extension.swift
================================================
import UIKit
extension Array {
func random() -> Generator.Element {
let index = Int(arc4random_uniform(UInt32(count)))
return self[index]
}
}
================================================
FILE: WhoToFollow/FollowTableViewCell.swift
================================================
import UIKit
import RxSwift
class FollowTableViewCell: UITableViewCell {
@IBOutlet weak var avatar: UIImageView!
@IBOutlet weak var name: UILabel!
@IBOutlet weak var cancel: UIButton!
var disposeBagCell:DisposeBag = DisposeBag()
override func prepareForReuse() {
disposeBagCell = DisposeBag()
}
}
================================================
FILE: WhoToFollow/FollowTableViewController.swift
================================================
import UIKit
import RxSwift
import RxCocoa
import Moya
class FollowTableViewController: UIViewController {
@IBOutlet weak var refresh: UIBarButtonItem!
@IBOutlet weak var tableView: UITableView!
var disposeBag = DisposeBag()
var provider: RxMoyaProvider! = RxMoyaProvider()
var dataSource = [User]()
var responseStream: Observable<[User]> = Observable.just([])
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView()
tableView.rowHeight = 70.0
rxBind()
}
func rxBind() {
let requestStream: Observable = refresh.rx_tap.startWith(())
.map { _ in
Array(1...1000).random()
}
responseStream = requestStream
.flatMap{ since in
UserModel(provider: self.provider).findUsers(since)
}
}
}
extension FollowTableViewController: UITableViewDataSource {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("FollowCell", forIndexPath: indexPath) as! FollowTableViewCell
cell.cancel.showsTouchWhenHighlighted = true
cell.avatar.layer.cornerRadius = cell.avatar.frame.size.width / 2
cell.avatar.clipsToBounds = true
let closeStream = cell.cancel.rx_tap.startWith(())
let userStream: Observable = Observable.combineLatest(
closeStream,
responseStream)
{ (_, users) in
guard users.count > 0 else {return nil}
return users.random()
}
let nilOnRefreshTapStream: Observable = refresh.rx_tap.map {_ in return nil}
let suggestionStream = Observable.of(userStream, nilOnRefreshTapStream)
.merge()
.startWith(.None)
suggestionStream.subscribeNext{ op in
guard let u = op else { return self.clearCell(cell) }
return self.setCell(cell, user: u )
}.addDisposableTo(cell.disposeBagCell)
return cell
}
func clearCell(cell: FollowTableViewCell) {
cell.cancel.hidden = true
cell.avatar.image = nil
cell.name.text = nil
}
func setCell(cell: FollowTableViewCell, user: User) {
clearCell(cell)
guard let url = NSURL(string: user.avatarUrl) else {return}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
guard let data = NSData(contentsOfURL: url) else {return}
dispatch_async(dispatch_get_main_queue(), {
cell.cancel.hidden = false
cell.avatar.image = UIImage(data: data)
cell.name.text = user.name
})
}
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
================================================
FILE: WhoToFollow/Github.swift
================================================
import Foundation
import Moya
enum GitHub {
case Users(since: Int)
}
extension GitHub: TargetType {
var baseURL: NSURL { return NSURL(string: "https://api.github.com")! }
var path: String {
switch self {
case .Users:
return "/users"
}
}
var method: Moya.Method {
return .GET
}
var parameters: [String: AnyObject]? {
switch self {
case .Users(let since): return ["since": since]
}
}
var sampleData: NSData {
return "".dataUsingEncoding(NSUTF8StringEncoding)!
}
}
================================================
FILE: WhoToFollow/Info.plist
================================================
CFBundleDevelopmentRegion
en
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
APPL
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
1
LSRequiresIPhoneOS
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
UIRequiredDeviceCapabilities
armv7
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
================================================
FILE: WhoToFollow/User.swift
================================================
import Mapper
struct User: Mappable {
let id: Int
let name: String
let avatarUrl: String
init(map: Mapper) throws {
try id = map.from("id")
try name = map.from("login")
try avatarUrl = map.from("avatar_url")
}
}
================================================
FILE: WhoToFollow/UserModel.swift
================================================
import Foundation
import RxSwift
import RxOptional
import Moya
import Moya_ModelMapper
struct UserModel {
let provider: RxMoyaProvider
func findUsers(since: Int) -> Observable<[User]> {
return self.provider
.request(GitHub.Users(since: since))
.debug()
.mapArrayOptional(User.self)
.replaceNilWith([])
}
}
================================================
FILE: WhoToFollow.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
148612841D3DDF3100DF017B /* Github.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148612831D3DDF3100DF017B /* Github.swift */; };
148612861D3DE17800DF017B /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148612851D3DE17800DF017B /* User.swift */; };
148612881D3DE4B300DF017B /* UserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148612871D3DE4B300DF017B /* UserModel.swift */; };
1486128A1D3DF86000DF017B /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148612891D3DF86000DF017B /* Extension.swift */; };
3B7E031D1122C07FC6305327 /* Pods_WhoToFollowTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32E7CEC02C92DD0D5BA3833E /* Pods_WhoToFollowTests.framework */; };
8FFA3362E6CE79F75508E8C1 /* Pods_WhoToFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 911461D15DA8DDD2D1181B20 /* Pods_WhoToFollow.framework */; };
B4DDDCFD1D3D26EB0050DAE0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DDDCFC1D3D26EB0050DAE0 /* AppDelegate.swift */; };
B4DDDD021D3D26EB0050DAE0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B4DDDD001D3D26EB0050DAE0 /* Main.storyboard */; };
B4DDDD041D3D26EB0050DAE0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B4DDDD031D3D26EB0050DAE0 /* Assets.xcassets */; };
B4DDDD071D3D26EB0050DAE0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B4DDDD051D3D26EB0050DAE0 /* LaunchScreen.storyboard */; };
B4DDDD121D3D26EB0050DAE0 /* WhoToFollowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DDDD111D3D26EB0050DAE0 /* WhoToFollowTests.swift */; };
B4DDDD2B1D3D29140050DAE0 /* FollowTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DDDD2A1D3D29140050DAE0 /* FollowTableViewController.swift */; };
B4DDDD2D1D3D29700050DAE0 /* FollowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DDDD2C1D3D29700050DAE0 /* FollowTableViewCell.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
B4DDDD0E1D3D26EB0050DAE0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B4DDDCF11D3D26EB0050DAE0 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B4DDDCF81D3D26EB0050DAE0;
remoteInfo = WhoToFollow;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
148612831D3DDF3100DF017B /* Github.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Github.swift; sourceTree = ""; };
148612851D3DE17800DF017B /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
148612871D3DE4B300DF017B /* UserModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserModel.swift; sourceTree = ""; };
148612891D3DF86000DF017B /* Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extension.swift; sourceTree = ""; };
32E7CEC02C92DD0D5BA3833E /* Pods_WhoToFollowTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WhoToFollowTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
911461D15DA8DDD2D1181B20 /* Pods_WhoToFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WhoToFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A580DEB4690AACCE59A49962 /* Pods-WhoToFollowTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WhoToFollowTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-WhoToFollowTests/Pods-WhoToFollowTests.debug.xcconfig"; sourceTree = ""; };
B4DDDCF91D3D26EB0050DAE0 /* WhoToFollow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WhoToFollow.app; sourceTree = BUILT_PRODUCTS_DIR; };
B4DDDCFC1D3D26EB0050DAE0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
B4DDDD011D3D26EB0050DAE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
B4DDDD031D3D26EB0050DAE0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
B4DDDD061D3D26EB0050DAE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
B4DDDD081D3D26EB0050DAE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
B4DDDD0D1D3D26EB0050DAE0 /* WhoToFollowTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WhoToFollowTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
B4DDDD111D3D26EB0050DAE0 /* WhoToFollowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhoToFollowTests.swift; sourceTree = ""; };
B4DDDD131D3D26EB0050DAE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
B4DDDD2A1D3D29140050DAE0 /* FollowTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FollowTableViewController.swift; sourceTree = ""; };
B4DDDD2C1D3D29700050DAE0 /* FollowTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FollowTableViewCell.swift; sourceTree = ""; };
BF4C7FEC726F2A8DC87CE165 /* Pods-WhoToFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WhoToFollow.release.xcconfig"; path = "Pods/Target Support Files/Pods-WhoToFollow/Pods-WhoToFollow.release.xcconfig"; sourceTree = ""; };
C78E3E8DA484E6DD5A3C8AAF /* Pods-WhoToFollowTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WhoToFollowTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-WhoToFollowTests/Pods-WhoToFollowTests.release.xcconfig"; sourceTree = ""; };
D3D2400B3852566FB54F323F /* Pods-WhoToFollow.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WhoToFollow.debug.xcconfig"; path = "Pods/Target Support Files/Pods-WhoToFollow/Pods-WhoToFollow.debug.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
B4DDDCF61D3D26EB0050DAE0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8FFA3362E6CE79F75508E8C1 /* Pods_WhoToFollow.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B4DDDD0A1D3D26EB0050DAE0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3B7E031D1122C07FC6305327 /* Pods_WhoToFollowTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
932A40A616BD85DA4971E7D8 /* Pods */ = {
isa = PBXGroup;
children = (
D3D2400B3852566FB54F323F /* Pods-WhoToFollow.debug.xcconfig */,
BF4C7FEC726F2A8DC87CE165 /* Pods-WhoToFollow.release.xcconfig */,
A580DEB4690AACCE59A49962 /* Pods-WhoToFollowTests.debug.xcconfig */,
C78E3E8DA484E6DD5A3C8AAF /* Pods-WhoToFollowTests.release.xcconfig */,
);
name = Pods;
sourceTree = "";
};
935C4CCAACADBEB1CB2817BE /* Frameworks */ = {
isa = PBXGroup;
children = (
911461D15DA8DDD2D1181B20 /* Pods_WhoToFollow.framework */,
32E7CEC02C92DD0D5BA3833E /* Pods_WhoToFollowTests.framework */,
);
name = Frameworks;
sourceTree = "";
};
B4DDDCF01D3D26EB0050DAE0 = {
isa = PBXGroup;
children = (
B4DDDCFB1D3D26EB0050DAE0 /* WhoToFollow */,
B4DDDD101D3D26EB0050DAE0 /* WhoToFollowTests */,
B4DDDCFA1D3D26EB0050DAE0 /* Products */,
932A40A616BD85DA4971E7D8 /* Pods */,
935C4CCAACADBEB1CB2817BE /* Frameworks */,
);
sourceTree = "";
};
B4DDDCFA1D3D26EB0050DAE0 /* Products */ = {
isa = PBXGroup;
children = (
B4DDDCF91D3D26EB0050DAE0 /* WhoToFollow.app */,
B4DDDD0D1D3D26EB0050DAE0 /* WhoToFollowTests.xctest */,
);
name = Products;
sourceTree = "";
};
B4DDDCFB1D3D26EB0050DAE0 /* WhoToFollow */ = {
isa = PBXGroup;
children = (
B4DDDCFC1D3D26EB0050DAE0 /* AppDelegate.swift */,
B4DDDD001D3D26EB0050DAE0 /* Main.storyboard */,
B4DDDD031D3D26EB0050DAE0 /* Assets.xcassets */,
B4DDDD051D3D26EB0050DAE0 /* LaunchScreen.storyboard */,
B4DDDD081D3D26EB0050DAE0 /* Info.plist */,
B4DDDD2A1D3D29140050DAE0 /* FollowTableViewController.swift */,
B4DDDD2C1D3D29700050DAE0 /* FollowTableViewCell.swift */,
148612831D3DDF3100DF017B /* Github.swift */,
148612851D3DE17800DF017B /* User.swift */,
148612871D3DE4B300DF017B /* UserModel.swift */,
148612891D3DF86000DF017B /* Extension.swift */,
);
path = WhoToFollow;
sourceTree = "";
};
B4DDDD101D3D26EB0050DAE0 /* WhoToFollowTests */ = {
isa = PBXGroup;
children = (
B4DDDD111D3D26EB0050DAE0 /* WhoToFollowTests.swift */,
B4DDDD131D3D26EB0050DAE0 /* Info.plist */,
);
path = WhoToFollowTests;
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
B4DDDCF81D3D26EB0050DAE0 /* WhoToFollow */ = {
isa = PBXNativeTarget;
buildConfigurationList = B4DDDD211D3D26EB0050DAE0 /* Build configuration list for PBXNativeTarget "WhoToFollow" */;
buildPhases = (
41F69BD4F0BCBF8009E84D8D /* [CP] Check Pods Manifest.lock */,
B4DDDCF51D3D26EB0050DAE0 /* Sources */,
B4DDDCF61D3D26EB0050DAE0 /* Frameworks */,
B4DDDCF71D3D26EB0050DAE0 /* Resources */,
F80817C19D7E9B4680BF8FD9 /* [CP] Embed Pods Frameworks */,
4D81FFBC746BCA351B0E6CBB /* [CP] Copy Pods Resources */,
9491843B804D34BF3C1C2EF2 /* Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = WhoToFollow;
productName = WhoToFollow;
productReference = B4DDDCF91D3D26EB0050DAE0 /* WhoToFollow.app */;
productType = "com.apple.product-type.application";
};
B4DDDD0C1D3D26EB0050DAE0 /* WhoToFollowTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = B4DDDD241D3D26EB0050DAE0 /* Build configuration list for PBXNativeTarget "WhoToFollowTests" */;
buildPhases = (
910BDE30B7501E8A2DE7FB0F /* [CP] Check Pods Manifest.lock */,
B4DDDD091D3D26EB0050DAE0 /* Sources */,
B4DDDD0A1D3D26EB0050DAE0 /* Frameworks */,
B4DDDD0B1D3D26EB0050DAE0 /* Resources */,
B0EB3FAB78B402532551BDE5 /* [CP] Embed Pods Frameworks */,
C63794B042EAF3F1B4969774 /* [CP] Copy Pods Resources */,
D5D1452E6E441C02FB3D10FD /* Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
B4DDDD0F1D3D26EB0050DAE0 /* PBXTargetDependency */,
);
name = WhoToFollowTests;
productName = WhoToFollowTests;
productReference = B4DDDD0D1D3D26EB0050DAE0 /* WhoToFollowTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
B4DDDCF11D3D26EB0050DAE0 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0730;
LastUpgradeCheck = 0730;
ORGANIZATIONNAME = DTVD;
TargetAttributes = {
B4DDDCF81D3D26EB0050DAE0 = {
CreatedOnToolsVersion = 7.3.1;
};
B4DDDD0C1D3D26EB0050DAE0 = {
CreatedOnToolsVersion = 7.3.1;
TestTargetID = B4DDDCF81D3D26EB0050DAE0;
};
};
};
buildConfigurationList = B4DDDCF41D3D26EB0050DAE0 /* Build configuration list for PBXProject "WhoToFollow" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = B4DDDCF01D3D26EB0050DAE0;
productRefGroup = B4DDDCFA1D3D26EB0050DAE0 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
B4DDDCF81D3D26EB0050DAE0 /* WhoToFollow */,
B4DDDD0C1D3D26EB0050DAE0 /* WhoToFollowTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
B4DDDCF71D3D26EB0050DAE0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B4DDDD071D3D26EB0050DAE0 /* LaunchScreen.storyboard in Resources */,
B4DDDD041D3D26EB0050DAE0 /* Assets.xcassets in Resources */,
B4DDDD021D3D26EB0050DAE0 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B4DDDD0B1D3D26EB0050DAE0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
41F69BD4F0BCBF8009E84D8D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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";
showEnvVarsInLog = 0;
};
4D81FFBC746BCA351B0E6CBB /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-WhoToFollow/Pods-WhoToFollow-resources.sh\"\n";
showEnvVarsInLog = 0;
};
910BDE30B7501E8A2DE7FB0F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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";
showEnvVarsInLog = 0;
};
9491843B804D34BF3C1C2EF2 /* Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-WhoToFollow/Pods-WhoToFollow-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
B0EB3FAB78B402532551BDE5 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-WhoToFollowTests/Pods-WhoToFollowTests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
C63794B042EAF3F1B4969774 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-WhoToFollowTests/Pods-WhoToFollowTests-resources.sh\"\n";
showEnvVarsInLog = 0;
};
D5D1452E6E441C02FB3D10FD /* Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-WhoToFollowTests/Pods-WhoToFollowTests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
F80817C19D7E9B4680BF8FD9 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-WhoToFollow/Pods-WhoToFollow-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
B4DDDCF51D3D26EB0050DAE0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B4DDDD2B1D3D29140050DAE0 /* FollowTableViewController.swift in Sources */,
B4DDDD2D1D3D29700050DAE0 /* FollowTableViewCell.swift in Sources */,
1486128A1D3DF86000DF017B /* Extension.swift in Sources */,
148612841D3DDF3100DF017B /* Github.swift in Sources */,
148612881D3DE4B300DF017B /* UserModel.swift in Sources */,
148612861D3DE17800DF017B /* User.swift in Sources */,
B4DDDCFD1D3D26EB0050DAE0 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B4DDDD091D3D26EB0050DAE0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B4DDDD121D3D26EB0050DAE0 /* WhoToFollowTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
B4DDDD0F1D3D26EB0050DAE0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B4DDDCF81D3D26EB0050DAE0 /* WhoToFollow */;
targetProxy = B4DDDD0E1D3D26EB0050DAE0 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
B4DDDD001D3D26EB0050DAE0 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
B4DDDD011D3D26EB0050DAE0 /* Base */,
);
name = Main.storyboard;
sourceTree = "";
};
B4DDDD051D3D26EB0050DAE0 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
B4DDDD061D3D26EB0050DAE0 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
B4DDDD1F1D3D26EB0050DAE0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
B4DDDD201D3D26EB0050DAE0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
B4DDDD221D3D26EB0050DAE0 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D3D2400B3852566FB54F323F /* Pods-WhoToFollow.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = WhoToFollow/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.vunhatminh.WhoToFollow;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
B4DDDD231D3D26EB0050DAE0 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BF4C7FEC726F2A8DC87CE165 /* Pods-WhoToFollow.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = WhoToFollow/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.vunhatminh.WhoToFollow;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
B4DDDD251D3D26EB0050DAE0 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A580DEB4690AACCE59A49962 /* Pods-WhoToFollowTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
INFOPLIST_FILE = WhoToFollowTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.vunhatminh.WhoToFollowTests;
PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WhoToFollow.app/WhoToFollow";
};
name = Debug;
};
B4DDDD261D3D26EB0050DAE0 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C78E3E8DA484E6DD5A3C8AAF /* Pods-WhoToFollowTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
INFOPLIST_FILE = WhoToFollowTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.vunhatminh.WhoToFollowTests;
PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WhoToFollow.app/WhoToFollow";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
B4DDDCF41D3D26EB0050DAE0 /* Build configuration list for PBXProject "WhoToFollow" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B4DDDD1F1D3D26EB0050DAE0 /* Debug */,
B4DDDD201D3D26EB0050DAE0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B4DDDD211D3D26EB0050DAE0 /* Build configuration list for PBXNativeTarget "WhoToFollow" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B4DDDD221D3D26EB0050DAE0 /* Debug */,
B4DDDD231D3D26EB0050DAE0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B4DDDD241D3D26EB0050DAE0 /* Build configuration list for PBXNativeTarget "WhoToFollowTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B4DDDD251D3D26EB0050DAE0 /* Debug */,
B4DDDD261D3D26EB0050DAE0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = B4DDDCF11D3D26EB0050DAE0 /* Project object */;
}
================================================
FILE: WhoToFollow.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: WhoToFollow.xcodeproj/xcuserdata/DTVD.xcuserdatad/xcschemes/WhoToFollow.xcscheme
================================================
================================================
FILE: WhoToFollow.xcodeproj/xcuserdata/DTVD.xcuserdatad/xcschemes/xcschememanagement.plist
================================================
SchemeUserState
WhoToFollow.xcscheme
orderHint
0
SuppressBuildableAutocreation
B4DDDCF81D3D26EB0050DAE0
primary
B4DDDD0C1D3D26EB0050DAE0
primary
B4DDDD171D3D26EB0050DAE0
primary
================================================
FILE: WhoToFollow.xcodeproj/xcuserdata/vunhat_minh.xcuserdatad/xcschemes/WhoToFollow.xcscheme
================================================
================================================
FILE: WhoToFollow.xcodeproj/xcuserdata/vunhat_minh.xcuserdatad/xcschemes/xcschememanagement.plist
================================================
SchemeUserState
WhoToFollow.xcscheme
orderHint
13
SuppressBuildableAutocreation
B4DDDCF81D3D26EB0050DAE0
primary
B4DDDD0C1D3D26EB0050DAE0
primary
B4DDDD171D3D26EB0050DAE0
primary
================================================
FILE: WhoToFollow.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: WhoToFollow.xcworkspace/xcuserdata/vunhat_minh.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
================================================
================================================
FILE: WhoToFollowTests/Info.plist
================================================
CFBundleDevelopmentRegion
en
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
BNDL
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
1
================================================
FILE: WhoToFollowTests/WhoToFollowTests.swift
================================================
//
// WhoToFollowTests.swift
// WhoToFollowTests
//
// Created by DTVD on 7/19/16.
// Copyright © 2016 DTVD. All rights reserved.
//
import XCTest
@testable import WhoToFollow
class WhoToFollowTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock {
// Put the code you want to measure the time of here.
}
}
}