├── Loading View Controllers.playground ├── Pages │ ├── Container View Controller.xcplaygroundpage │ │ ├── Contents.swift │ │ └── timeline.xctimeline │ ├── Protocol.xcplaygroundpage │ │ ├── Contents.swift │ │ └── timeline.xctimeline │ └── Starting Point.xcplaygroundpage │ │ ├── Contents.swift │ │ └── timeline.xctimeline ├── Sources │ ├── Helpers.swift │ └── Networking.swift ├── contents.xcplayground └── playground.xcworkspace │ └── contents.xcworkspacedata ├── README.md └── episode.json /Loading View Controllers.playground/Pages/Container View Controller.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: To run this playground start a SimpleHTTPServer on the commandline like this: 2 | //: 3 | //: `python -m SimpleHTTPServer 8000` 4 | //: 5 | //: It will serve up the current directory, so make sure to be in the directory containing episode.json 6 | 7 | import UIKit 8 | 9 | let url = NSURL(string: "http://localhost:8000/episode.json")! 10 | let episodeResource = Resource(url: url, parseJSON: { anyObject in 11 | (anyObject as? JSONDictionary).flatMap(Episode.init) 12 | }) 13 | 14 | 15 | final class LoadingViewController: UIViewController { 16 | let spinner = UIActivityIndicatorView(activityIndicatorStyle: .Gray) 17 | 18 | init(load: ((Result) -> ()) -> (), build: (A) -> UIViewController) { 19 | super.init(nibName: nil, bundle: nil) 20 | spinner.startAnimating() 21 | load() { [weak self] result in 22 | self?.spinner.stopAnimating() 23 | guard let value = result.value else { return } // TODO loading error 24 | let viewController = build(value) 25 | self?.add(content: viewController) 26 | } 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | view.backgroundColor = .whiteColor() 32 | spinner.hidesWhenStopped = true 33 | spinner.translatesAutoresizingMaskIntoConstraints = false 34 | view.addSubview(spinner) 35 | spinner.center(inView: view) 36 | } 37 | 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | func add(content content: UIViewController) { 43 | addChildViewController(content) 44 | view.addSubview(content.view) 45 | content.view.translatesAutoresizingMaskIntoConstraints = false 46 | content.view.constrainEdges(toMarginOf: view) 47 | content.didMoveToParentViewController(self) 48 | } 49 | } 50 | 51 | 52 | final class EpisodeDetailViewController: UIViewController { 53 | let titleLabel = UILabel() 54 | 55 | convenience init(episode: Episode) { 56 | self.init() 57 | titleLabel.text = episode.title 58 | } 59 | 60 | override func viewDidLoad() { 61 | view.backgroundColor = .whiteColor() 62 | 63 | view.addSubview(titleLabel) 64 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 65 | titleLabel.constrainEdges(toMarginOf: view) 66 | } 67 | } 68 | 69 | 70 | let sharedWebservice = Webservice() 71 | 72 | let episodesVC = LoadingViewController(load: { callback in 73 | sharedWebservice.load(episodeResource, completion: callback) 74 | }, build: EpisodeDetailViewController.init) 75 | 76 | episodesVC.view.frame = CGRect(x: 0, y: 0, width: 250, height: 300) 77 | 78 | 79 | import XCPlayground 80 | XCPlaygroundPage.currentPage.liveView = episodesVC 81 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/Pages/Container View Controller.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/Pages/Protocol.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: To run this playground start a SimpleHTTPServer on the commandline like this: 2 | //: 3 | //: `python -m SimpleHTTPServer 8000` 4 | //: 5 | //: It will serve up the current directory, so make sure to be in the directory containing episode.json 6 | 7 | import UIKit 8 | 9 | let url = NSURL(string: "http://localhost:8000/episode.json")! 10 | let episodeResource = Resource(url: url, parseJSON: { anyObject in 11 | (anyObject as? JSONDictionary).flatMap(Episode.init) 12 | }) 13 | 14 | 15 | let sharedWebservice = Webservice() 16 | 17 | 18 | protocol Loading { 19 | associatedtype ResourceType 20 | var spinner: UIActivityIndicatorView { get } 21 | func configure(value: ResourceType) 22 | } 23 | 24 | extension Loading where Self: UIViewController { 25 | func load(resource: Resource) { 26 | spinner.startAnimating() 27 | sharedWebservice.load(resource) { [weak self] result in 28 | self?.spinner.stopAnimating() 29 | guard let value = result.value else { return } // TODO loading error 30 | self?.configure(value) 31 | } 32 | } 33 | } 34 | 35 | 36 | final class EpisodeDetailViewController: UIViewController, Loading { 37 | let spinner = UIActivityIndicatorView(activityIndicatorStyle: .Gray) 38 | let titleLabel = UILabel() 39 | 40 | convenience init(episode: Episode) { 41 | self.init() 42 | configure(episode) 43 | } 44 | 45 | convenience init(resource: Resource) { 46 | self.init() 47 | load(resource) 48 | } 49 | 50 | func configure(value: Episode) { 51 | titleLabel.text = value.title 52 | } 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | view.backgroundColor = .whiteColor() 57 | 58 | spinner.hidesWhenStopped = true 59 | spinner.translatesAutoresizingMaskIntoConstraints = false 60 | view.addSubview(spinner) 61 | spinner.center(inView: view) 62 | 63 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 64 | view.addSubview(titleLabel) 65 | titleLabel.constrainEdges(toMarginOf: view) 66 | } 67 | } 68 | 69 | 70 | let episodesVC = EpisodeDetailViewController(resource: episodeResource) 71 | episodesVC.view.frame = CGRect(x: 0, y: 0, width: 250, height: 300) 72 | 73 | 74 | import XCPlayground 75 | XCPlaygroundPage.currentPage.liveView = episodesVC 76 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/Pages/Protocol.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/Pages/Starting Point.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: To run this playground start a SimpleHTTPServer on the commandline like this: 2 | //: 3 | //: `python -m SimpleHTTPServer 8000` 4 | //: 5 | //: It will serve up the current directory, so make sure to be in the directory containing episode.json 6 | 7 | import UIKit 8 | 9 | let url = NSURL(string: "http://localhost:8000/episode.json")! 10 | let episodeResource = Resource(url: url, parseJSON: { anyObject in 11 | (anyObject as? JSONDictionary).flatMap(Episode.init) 12 | }) 13 | 14 | 15 | let sharedWebservice = Webservice() 16 | 17 | 18 | final class EpisodeDetailViewController: UIViewController { 19 | let spinner = UIActivityIndicatorView(activityIndicatorStyle: .Gray) 20 | let titleLabel = UILabel() 21 | 22 | convenience init(episode: Episode) { 23 | self.init() 24 | titleLabel.text = episode.title 25 | } 26 | 27 | convenience init(resource: Resource) { 28 | self.init() 29 | spinner.startAnimating() 30 | sharedWebservice.load(resource) { [weak self] result in 31 | self?.spinner.stopAnimating() 32 | guard let value = result.value else { return } // TODO loading error 33 | self?.titleLabel.text = value.title 34 | } 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | view.backgroundColor = .whiteColor() 40 | 41 | spinner.hidesWhenStopped = true 42 | spinner.translatesAutoresizingMaskIntoConstraints = false 43 | view.addSubview(spinner) 44 | spinner.center(inView: view) 45 | 46 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 47 | view.addSubview(titleLabel) 48 | titleLabel.constrainEdges(toMarginOf: view) 49 | } 50 | } 51 | 52 | 53 | let episodesVC = EpisodeDetailViewController(resource: episodeResource) 54 | episodesVC.view.frame = CGRect(x: 0, y: 0, width: 250, height: 300) 55 | 56 | 57 | import XCPlayground 58 | XCPlaygroundPage.currentPage.liveView = episodesVC 59 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/Pages/Starting Point.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/Sources/Helpers.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | public func constrainEqual(attribute: NSLayoutAttribute, to: AnyObject, multiplier: CGFloat = 1, constant: CGFloat = 0) { 5 | constrainEqual(attribute, to: to, attribute, multiplier: multiplier, constant: constant) 6 | } 7 | 8 | public func constrainEqual(attribute: NSLayoutAttribute, to: AnyObject, _ toAttribute: NSLayoutAttribute, multiplier: CGFloat = 1, constant: CGFloat = 0) { 9 | NSLayoutConstraint.activateConstraints([ 10 | NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .Equal, toItem: to, attribute: toAttribute, multiplier: multiplier, constant: constant) 11 | ] 12 | ) 13 | } 14 | 15 | public func constrainEdges(toMarginOf view: UIView) { 16 | constrainEqual(.Top, to: view, .TopMargin) 17 | constrainEqual(.Leading, to: view, .LeadingMargin) 18 | constrainEqual(.Trailing, to: view, .TrailingMargin) 19 | constrainEqual(.Bottom, to: view, .BottomMargin) 20 | } 21 | 22 | public func center(inView view: UIView) { 23 | centerXAnchor.constrainEqual(view.centerXAnchor) 24 | centerYAnchor.constrainEqual(view.centerYAnchor) 25 | } 26 | } 27 | 28 | extension NSLayoutAnchor { 29 | public func constrainEqual(anchor: NSLayoutAnchor, constant: CGFloat = 0) { 30 | let constraint = constraintEqualToAnchor(anchor, constant: constant) 31 | constraint.active = true 32 | } 33 | } 34 | 35 | 36 | public func mainQueue(block: () -> ()) { 37 | dispatch_async(dispatch_get_main_queue(), block) 38 | } 39 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/Sources/Networking.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public struct Episode { 5 | public var id: String 6 | public var title: String 7 | 8 | public init(id: String, title: String) { 9 | self.id = id 10 | self.title = title 11 | } 12 | } 13 | 14 | 15 | public typealias JSONDictionary = [String: AnyObject] 16 | 17 | extension Episode { 18 | public init?(json: JSONDictionary) { 19 | guard let id = json["id"] as? String, 20 | title = json["title"] as? String else { return nil } 21 | 22 | self.id = id 23 | self.title = title 24 | } 25 | } 26 | 27 | 28 | public struct Resource { 29 | public var url: NSURL 30 | public var parse: NSData -> A? 31 | } 32 | 33 | extension Resource { 34 | public init(url: NSURL, parseJSON: AnyObject -> A?) { 35 | self.url = url 36 | self.parse = { data in 37 | let json = try? NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) 38 | return json.flatMap(parseJSON) 39 | } 40 | } 41 | } 42 | 43 | 44 | public enum Result { 45 | case success(A) 46 | case error(ErrorType) 47 | } 48 | 49 | extension Result { 50 | public init(_ value: A?, or error: ErrorType) { 51 | if let value = value { 52 | self = .success(value) 53 | } else { 54 | self = .error(error) 55 | } 56 | } 57 | 58 | public var value: A? { 59 | guard case .success(let v) = self else { return nil } 60 | return v 61 | } 62 | } 63 | 64 | 65 | public enum WebserviceError: ErrorType { 66 | case other 67 | } 68 | 69 | 70 | public final class Webservice { 71 | public init() { } 72 | 73 | /// Loads a resource. The completion handler is always called on the main queue. 74 | public func load(resource: Resource, completion: Result -> ()) { 75 | NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, response, _ in 76 | let parsed = data.flatMap(resource.parse) 77 | let result = Result(parsed, or: WebserviceError.other) 78 | mainQueue { completion(result) } 79 | }.resume() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Loading View Controllers.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loading View Controllers 2 | 3 | This is the code that accompanies Swift Talk Episode 3: [Loading View Controllers](https://talk.objc.io/episodes/S01E03-loading-view-controllers) 4 | -------------------------------------------------------------------------------- /episode.json: -------------------------------------------------------------------------------- 1 | {"id":"20718f6c-08ca-473d-bd72-ae2d5a00085f","title":"Networking"} --------------------------------------------------------------------------------