Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Painless, Type-Safe Data Views with Swift and I...

Avatar for Jordan Kay Jordan Kay
October 30, 2015

Painless, Type-Safe Data Views with Swift and Interface Builder

Avatar for Jordan Kay

Jordan Kay

October 30, 2015

Other Decks in Programming

Transcript

  1. Mensa github.com/jordanekay/Mensa • Easily represent tables and collections of data

    • Abstract away from UITableViewController and UICollectionViewController • Define views using Interface Builder and Auto Layout • Map data models to views used to display them, and view controllers to handle display and interaction
  2. Numbers and Primes • Display a list of positive numbers

    in increasing order • Below each number that's a prime, show a flag to indicate • Tapping on a number should display its factors
  3. Numbers and Primes • The larger the number, the smaller

    it should be displayed • In certain contexts, we may want to display items differently
  4. Existing Solutions • How do we usually solve this problem?

    • What are some paradigms we might use? • Which challenges may arise?
  5. Number struct Number { let value: Int lazy var prime:

    Bool = { self.factors.count == 2 }() lazy var factors: [Int] = { var factors = Set<Int>() ... return factors.sort() }() init(_ value: Int) { self.value = value } }
  6. Prime Flag View class PrimeFlagView: UIView { enum Style: Int

    { case Default case Compact } @IBOutlet private(set) weak var textLabel: UILabel? private(set) var formatString: String! override func awakeFromNib() { formatString = textLabel?.text } }
  7. Number View Controller class NumberViewController: HostedViewController<Number, NumberView> { override func

    updateView(view: NumberView, withObject number: Number) { view.valueLabel.text = "\(number.value)" } override func selectObject(var number: Number) { let factorsString = number.factors.map { "\($0)" }.joinWithSeparator(", ") let message = "The factors of \(number.value) are \(factorsString)." let alertController = UIAlertController(title: nil, message: message, preferredStyle: .Alert) let dismissAction = UIAlertAction(title: "Dismiss", style: .Cancel, handler: nil) alertController.addAction(dismissAction) presentViewController(alertController, animated: true, completion: nil) } }
  8. Prime Flag View Controller class PrimeFlagViewController: HostedViewController<PrimeFlag, PrimeFlagView> { override

    func updateView(view: PrimeFlagView, withObject primeFlag: PrimeFlag) { view.textLabel?.text = String(format: view.formatString, arguments: [primeFlag.number.value]) } }
  9. Table View Controller class ObjectTableViewController: TableViewController<Number, NumberView> { private var

    numbers: [Number] = [] override var sections: [Section<Number>] { return [Section(numbers)] } required init(style: UITableViewStyle) { numbers = (1...numberCount).map { Number($0) } super.init(style: style) } }
  10. Table View Controller class ObjectTableViewController: TableViewController<Object, UIView> { private var

    objects: [Object] = [] override var sections: [Section<Object>] { return [Section(objects)] } required init(style: UITableViewStyle) { for index in (1...numberCount) { var number = Number(index) objects.append(number) if number.prime { objects.append(PrimeFlag(number: number)) } } super.init(style: style) } }
  11. View Controller Registration override static func registerViewControllers() throws { try

    registerViewControllerClass(NumberViewController.self, forModelType: Number.self) try registerViewControllerClass(PrimeFlagViewController.self, forModelType: PrimeFlag.self) }
  12. Hosted View Controller Customization Points func updateView(view: View, withObject object:

    Object) func selectObject(object: Object) func canSelectObject(object: Object) -> Bool func setViewHighlighted(highlighted: Bool, forObject: Object)
  13. Hosting View Controller Customization Points func didSelectObject(object: Object) func willLoadHostedViewController(viewController:

    HostedViewController<Object, View>) func didUseViewController(viewController: HostedViewController<Object, View>, withObject object: Object) func variantForObject(object: Object) -> Int
  14. Decreasing the Font Size override func didUseViewController(viewController: HostedViewController<Object, UIView>, withObject

    object: Object) { if let number = object as? Number, view = viewController.view as? NumberView { let fontSize = CGFloat(maxFontSize - number.value) view.valueLabel.font = view.valueLabel.font.fontWithSize(fontSize) } }
  15. Choosing a Variant override func variantForObject(object: Object) -> Int {

    if object is PrimeFlag { return PrimeFlagView.Style.Compact.rawValue } return super.variantForObject(object) }
  16. Metrics Cells private var backingSections: [Section<Object>] = [] { didSet

    { for section in backingSections { for object in section { let reuseIdentifier = reuseIdentifierForObject(object) if metricsCells[reuseIdentifier] == nil { if let metricsCell = createMetricsCellForObject(object) { metricsCells[reuseIdentifier] = metricsCell } } } } } }
  17. Runtime Cell Subclassing public static func subclassWithViewControllerClass(viewControllerClass: HostedViewController<ObjectType, ViewType>.Type, modelType:

    ObjectType.Type, variant: Int) -> CellClass { let bundle = NSBundle(forClass: self) let modelTypeName = TypeKey(modelType).localDescription let className = TypeKey<Any>(self, viewControllerClass, modelType).description var subclass: AnyClass? = NSClassFromString(className) if subclass == nil { subclass = objc_allocateClassPair(self, className.cStringUsingEncoding(NSUTF8StringEncoding)!, 0) let block: @convention(block) AnyObject -> UIViewController = { _ in let nibName = modelTypeName + "View" let viewController = (viewControllerClass as UIViewController.Type).init() let contents = bundle.loadNibNamed(nibName, owner: viewController, options: nil) viewController.view = contents[variant] as! UIView return viewController } let implementation = imp_implementationWithBlock(unsafeBitCast(block, AnyObject.self)); class_addMethod(subclass, "hostedViewController", implementation, "#@:"); objc_registerClassPair(subclass); } return subclass as! CellClass }
  18. View Hosting func loadHostedViewForObject<Object, View: UIView, Cell: HostingCell where Object

    == Cell.ObjectType, View == Cell.ViewType>(object: Object, inCell cell: Cell) { let hostedView = cell.hostedViewController.view hostedView.frame = cell.hostingView.bounds hostedView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] cell.hostingView.addSubview(hostedView) }
  19. Metacritic API { "results": [ { "rlsdate": "2015-01-26", "author": "Gaz

    Coombes", "score": "83", "name": "Matador", }, { "rlsdate": "2015-01-26", "author": "The Phantom Band", "score": "TBD", "name": "Fears Trending", } ] }
  20. Media typealias Base = (title: String, score: Int?, releaseDate: NSDate)

    protocol Media: Decodable { var title: String { get } var score: Int? { get } var releaseDate: NSDate { get } static func baseDecode(j: AnyObject) throws -> Base }
  21. Media extension Media { static func baseDecode(j: AnyObject) throws ->

    Base { return try ( j => "name", Int(j => "score"), dateFormatter.dateFromString(j => "rlsdate")! ) } }
  22. Media List struct MediaList<T: Media> { let items: [T] static

    func resourceWithName(name: String) -> Resource<MediaList> { let path = "\(name)/new-releases" return Resource(path: path, method: .GET) { data -> MediaList? in let json = try! NSJSONSerialization.JSONObjectWithData( data, options: NSJSONReadingOptions(rawValue: 0)) return try? MediaList.decode(json) } } }
  23. Media List extension MediaList: Decodable { static func decode(j: AnyObject)

    throws -> MediaList { return try MediaList( items: j => "results" ) } }
  24. Metacritic API typealias FetchAlbumListTask = Task<Float, MediaList<Album>, Reason> typealias FetchMovieListTask

    = Task<Float, MediaList<Movie>, Reason> public struct MetacriticAPI: API { public static var baseURL: NSURL { return NSURL(string: "https://metacritic-2.p.mashape.com")! } static func fetchAlbumList() -> FetchAlbumListTask { return request(MediaList.resourceWithName("album-list")) } static func fetchMovieList() -> FetchMovieListTask { return request(MediaList.resourceWithName("movie-list")) } }
  25. SwiftTask • Promise + progress + pause + cancel +

    retry for Swift. • https://github.com/ReactKit/SwiftTask
  26. Album struct Album { let title: String let artist: String

    let score: Int? let releaseDate: NSDate } extension Album: Media { static func decode(j: AnyObject) throws -> Album { let artist: String = try j => "author" let base = try baseDecode(j) return Album(title: base.title, artist: artist, score: base.score, releaseDate: base.releaseDate) } }
  27. Movie struct Movie { let title: String let score: Int?

    let rating: String? let runtime: String let genre: String let releaseDate: NSDate }
  28. Movie extension Movie: Media { static func decode(j: AnyObject) throws

    -> Movie { let base = try baseDecode(j) let runtime: String = try j => "runtime" let genre: String = try j => "genre" var rating: String? = try j => "rating" if rating == "" || rating == "Not Rated" { rating = nil } return Movie(title: base.title, score: base.score, rating: rating, runtime: runtime, genre: genre, releaseDate: base.releaseDate) } }
  29. Album View class AlbumView: UIView { @IBOutlet private(set) weak var

    scoreLabel: UILabel! @IBOutlet private(set) weak var titleLabel: UILabel! @IBOutlet private(set) weak var artistLabel: UILabel! @IBOutlet private(set) weak var releaseDateLabel: UILabel! }
  30. Movie View class MovieView: UIView { @IBOutlet private(set) weak var

    scoreLabel: UILabel! @IBOutlet private(set) weak var titleLabel: UILabel! @IBOutlet private(set) weak var descriptionLabel: UILabel! @IBOutlet private(set) weak var genreLabel: UILabel! @IBOutlet private(set) weak var releaseDateLabel: UILabel! }
  31. Movie View Controller class MovieViewController: HostedViewController<Movie, MovieView> { override func

    updateView(view: MovieView, withObject movie: Movie) { view.titleLabel.text = movie.title view.scoreLabel.text = movie.score.map { "\($0)" } ?? "TBD" } }
  32. Album View Controller private var dateFormatter: NSDateFormatter = { let

    formatter = NSDateFormatter() formatter.dateFormat = "MM/d/yyyy" return formatter }() class AlbumViewController: HostedViewController<Album, AlbumView> { override func updateView(view: AlbumView, withObject album: Album) { view.titleLabel.text = album.title view.artistLabel.text = album.artist view.scoreLabel.text = album.score.map { "\($0)" } ?? "TBD" view.releaseDateLabel.text = "Released \(dateFormatter.stringFromDate(album.releaseDate))" } }
  33. Media View Controller class MediaListViewController: TableViewController<Media, UIView> { private var

    items: [Media] = [] override var sections: [Section<Media>] { let sortedItems = items.sort { $0.releaseDate.compare($1.releaseDate) == .OrderedDescending } return [Section(sortedItems)] } }
  34. Media View Controller override static func registerViewControllers() throws { try

    registerViewControllerClass(MovieViewController.self, forModelType: Movie.self) try registerViewControllerClass(AlbumViewController.self, forModelType: Album.self) }
  35. Media View Controller override func viewDidLoad() { super.viewDidLoad() MetacriticAPI.fetchAlbumList().success {

    albumList in self.items.appendContentsOf(albumList.items.map { $0 }) }.then { _ in MetacriticAPI.fetchMovieList() }.success { movieList in self.items.appendContentsOf(movieList.items.map { $0 }) dispatch_async(dispatch_get_main_queue()) { self.updateDataAndReloadTableView() } } }
  36. Conclusion • Model view controller can still be a sensible

    way to architect your app • Abstract away from UIKit APIs into more expressive logic • Define custom views, not custom cells • Allow for robust model display • Auto Layout is your friend