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

Swift ZHI - Open Source Swift App

Swift ZHI - Open Source Swift App

Swift ZHI is a news reader app for ZhiHu Daily.

Presented at 7th July 2015 at CocoaHeads Brisbane meetup.

Nicholas T.

July 07, 2015
Tweet

Other Decks in Programming

Transcript

  1. Swift ZHI Open source App, written in Swift Swift Nicholas

    Tian #SwiftZHI github.com/NicholasTD07 https://github.com/NicholasTD07/Swift-ZHI
  2. isPoorDesign() // true • UI • Logic • Downloading •

    Decoding • Spaghetti Code | GOD Controller
  3. redesign() -> • Swift-ZHI • SwiftDailyAPI • JSON Decoding -

    Argo • Network - Alamofire • Realm • UI
  4. redesign() -> • Swift-ZHI • SwiftDailyAPI • JSON Decoding -

    Argo • Network - Alamofire • Realm • UI • BDD Testing - Quick • Matcher - Nimble
  5. import Quick import Nimble class TableOfContentsSpec: QuickSpec { override func

    spec() { describe("the 'Documentation' directory") { it("has everything you need to get started") { let sections = Directory("Documentation").sections expect(sections).to(contain("Organized Tests with Quick Examples and Example Groups")) expect(sections).to(contain("Installing Quick")) } context("if it doesn't have what you're looking for") { it("needs to be updated") { let you = You(awesome: true) expect{you.submittedAnIssue}.toEventually(beTruthy()) } } } } }
  6. class DailyAPISpecs: QuickSpec { override func spec() { let timeout:

    NSTimeInterval = 20 let api = DailyAPI(userAgent: "SwiftDailySpec") it("loads daily news for a date") { var daily: Daily? = nil let date = NSDate.dateFromString("20150525", format: "yyyyMMdd")! api.daily(forDate: date) { dailyFromAPI in daily = dailyFromAPI } expect(daily).toEventuallyNot(beNil(), timeout: timeout) expect(daily!.date).toEventually(equal(date), timeout: timeout) expect(daily!.news).toEventuallyNot(beEmpty(), timeout: timeout) } } }
  7. Quick VS XCTest Quick * Better error message * fit

    - focus, xit - disable * More readable
  8. Quick VS XCTest Quick * Better error message * fit

    - focus, xit - disable * More readable XCTest * Performance Test * UI Test
  9. • Abstract Difference between imperative and functional programming? • WHAT

    THE HECK ARE THOSE OPERATORS • More about what than how
  10. • Abstract Difference between imperative and functional programming? • WHAT

    THE HECK ARE THOSE OPERATORS • ARHHHHHHHHH.. - Nick’s first day with Argo • More about what than how
  11. • Abstract Difference between imperative and functional programming? • WHAT

    THE HECK ARE THOSE OPERATORS • ARHHHHHHHHH.. - Nick’s first day with Argo • "Brain, offline." • More about what than how
  12. Syntax highlighted code: Xcode -> TextEdit -> Keynote public struct

    NewsMeta { public let newsId: Int public let title: String public let imageURLs: [NSURL] } { "id": 12345, "title": "Title of the News", "images": ["http://image.url"] }
  13. Syntax highlighted code: Xcode -> TextEdit -> Keynote public struct

    NewsMeta { public let newsId: Int public let title: String public let imageURLs: [NSURL] } static func decode(j: JSON) -> Decoded<NewsMeta> { return NewsMeta.create <^> j <| "id" <*> j <| "title" <*> j <|| "images" } { "id": 12345, "title": "Title of the News", "images": ["http://image.url"] }
  14. Syntax highlighted code: Xcode -> TextEdit -> Keynote static func

    decode(j: JSON) -> Decoded<NewsMeta> { return NewsMeta.create <^> j <| "id" <*> j <| "title" <*> j <|| "images" } extension NewsMeta: Decodable { // Curried `init` private static func create(newsId: Int)(title: String) (imageURLs: [NSURL]) -> NewsMeta { return NewsMeta( newsId: newsId, title: title, imageURLs: imageURLs) } } { "id": 12345, "title": "Title of the News", "images": ["http://image.url"] }
  15. /** map a function over an optional value - If

    the value is .None, the function will not be evaluated and this will return .None - If the value is .Some, the function will be applied to the unwrapped value :param: f A transformation function from type T to type U :param: a A value of type Optional<T> :returns: A value of type Optional<U> */ return NewsMeta.create <^> (j <| "id") public func <^><T, U>(f: T -> U, a: T?) -> U?
  16. /** map a function over an optional value - If

    the value is .None, the function will not be evaluated and this will return .None - If the value is .Some, the function will be applied to the unwrapped value :param: f A transformation function from type T to type U :param: a A value of type Optional<T> :returns: A value of type Optional<U> */ return NewsMeta.create <^> (j <| "id") public func <^><T, U>(f: T -> U, a: T?) -> U? Q: What’s the type of U in this case?
  17. /** map a function over an optional value - If

    the value is .None, the function will not be evaluated and this will return .None - If the value is .Some, the function will be applied to the unwrapped value :param: f A transformation function from type T to type U :param: a A value of type Optional<T> :returns: A value of type Optional<U> */ return NewsMeta.create <^> (j <| "id") public func <^><T, U>(f: T -> U, a: T?) -> U? A: Partially Applied create function Q: What’s the type of U in this case?
  18. return NewsMeta.create <^> (j <| "id") public func <^><T, U>(f:

    T -> U, a: T?) -> U? A: Partially Applied create function Q: What’s the type of U in this case? (title: String)(imageURLs: [NSURL]) -> NewsMeta
  19. return NewsMeta.create <^> (j <| "id") <*> j <| "title"

    <*> j <|| "images" public func <*><T, U>(f: (T -> U)?, a: T?) -> U? public func <^><T, U>(f: T -> U, a: T?) -> U?
  20. return NewsMeta.create <^> (j <| "id") <*> j <| "title"

    <*> j <|| "images" /** apply an optional function to an optional value - If either the value or the function are .None, the function will not be evaluated and this will return .None - If both the value and the function are .Some, the function will be applied to the unwrapped value :param: f An optional transformation function from type T to type U :param: a A value of type Optional<T> :returns: A value of type Optional<U> */ public func <*><T, U>(f: (T -> U)?, a: T?) -> U? public func <^><T, U>(f: T -> U, a: T?) -> U?
  21. /** flatMap a function over an optional value (left associative)

    - If the value is .None, the function will not be evaluated and this will return .None - If the value is .Some, the function will be applied to the unwrapped value :param: f A transformation function from type T to type Optional<U> :param: a A value of type Optional<T> :returns: A value of type Optional<U> */ public func >>-<T, U>(a: T?, f: T -> U?) -> U?
  22. Syntax highlighted code: Xcode -> TextEdit -> Keynote public struct

    News { public let newsId: Int public let title: String public let body: String public let cssURLs: [NSURL] public let imageURL: NSURL public let imageSourceText: String public let shareURL: NSURL }
  23. Syntax highlighted code: Xcode -> TextEdit -> Keynote public struct

    News { public let newsId: Int public let title: String public let body: String public let cssURLs: [NSURL] public let imageURL: NSURL public let imageSourceText: String public let shareURL: NSURL } public static func decode(j: JSON) -> Decoded<News> { return News.create <^> j <| "id" <*> j <| "title" <*> j <| "body" <*> j <|| "css" <*> j <| "image" <*> j <| "image_source" <*> j <| "share_url" }
  24. extension NSURL: Decodable { public static func decode(j: JSON) ->

    Decoded<NSURL> { switch(j) { case let .String(s): return Decoded<NSURL>.fromOptional(NSURL(string: s)) default: return .TypeMismatch("\(j) is not a NSURL.") } } }
  25. extension NSURL: Decodable { public static func decode(j: JSON) ->

    Decoded<NSURL> { switch(j) { case let .String(s): return Decoded<NSURL>.fromOptional(NSURL(string: s)) default: return .TypeMismatch("\(j) is not a NSURL.") } } } 20150707
  26. extension NSURL: Decodable { public static func decode(j: JSON) ->

    Decoded<NSURL> { switch(j) { case let .String(s): return Decoded<NSURL>.fromOptional(NSURL(string: s)) default: return .TypeMismatch("\(j) is not a NSURL.") } } } 20150707 public func toNSDate(format: String)(dateString: String) -> Decoded<NSDate> { return .fromOptional(NSDate.dateFromString(dateString, format: format)) }
  27. extension NSURL: Decodable { public static func decode(j: JSON) ->

    Decoded<NSURL> { switch(j) { case let .String(s): return Decoded<NSURL>.fromOptional(NSURL(string: s)) default: return .TypeMismatch("\(j) is not a NSURL.") } } } 20150707 public func toNSDate(format: String)(dateString: String) -> Decoded<NSDate> { return .fromOptional(NSDate.dateFromString(dateString, format: format)) } <^> ((j <| "date") >>- toNSDate("yyyyMMdd")) create
  28. extension NSURL: Decodable { public static func decode(j: JSON) ->

    Decoded<NSURL> { switch(j) { case let .String(s): return Decoded<NSURL>.fromOptional(NSURL(string: s)) default: return .TypeMismatch("\(j) is not a NSURL.") } } } 20150707 public func toNSDate(format: String)(dateString: String) -> Decoded<NSDate> { return .fromOptional(NSDate.dateFromString(dateString, format: format)) } <^> ((j <| "date") >>- toNSDate("yyyyMMdd")) step 1 create
  29. extension NSURL: Decodable { public static func decode(j: JSON) ->

    Decoded<NSURL> { switch(j) { case let .String(s): return Decoded<NSURL>.fromOptional(NSURL(string: s)) default: return .TypeMismatch("\(j) is not a NSURL.") } } } public func toNSDate(format: String)(dateString: String) -> Decoded<NSDate> { return .fromOptional(NSDate.dateFromString(dateString, format: format)) } <^> ((j <| "date") >>- toNSDate("yyyyMMdd")) step 1 "20150707" create
  30. extension NSURL: Decodable { public static func decode(j: JSON) ->

    Decoded<NSURL> { switch(j) { case let .String(s): return Decoded<NSURL>.fromOptional(NSURL(string: s)) default: return .TypeMismatch("\(j) is not a NSURL.") } } } public func toNSDate(format: String)(dateString: String) -> Decoded<NSDate> { return .fromOptional(NSDate.dateFromString(dateString, format: format)) } step 1 step 2 <^> ((j <| "date") >>- toNSDate("yyyyMMdd")) "20150525" NSDate.dateFromString("20150707", format: "yyyyMMdd") create
  31. extension NSURL: Decodable { public static func decode(j: JSON) ->

    Decoded<NSURL> { switch(j) { case let .String(s): return Decoded<NSURL>.fromOptional(NSURL(string: s)) default: return .TypeMismatch("\(j) is not a NSURL.") } } } public func toNSDate(format: String)(dateString: String) -> Decoded<NSDate> { return .fromOptional(NSDate.dateFromString(dateString, format: format)) } step 3 step 1 step 2 <^> ((j <| "date") >>- toNSDate("yyyyMMdd")) "20150525" NSDate.dateFromString("20150707", format: "yyyyMMdd") create
  32. Alamofire.request(.GET, URLString: "http://httpbin.org/ get", parameters: ["foo": "bar"]) .response { (request,

    response, data, error) in print(request) print(response) print(error) } Fluent API with Swift
  33. Alamofire.request(.GET, URLString: "http://httpbin.org/ get", parameters: ["foo": "bar"]) .response { (request,

    response, data, error) in print(request) print(response) print(error) } Fluent API with Swift Alamofire.request(.GET, URLString: "http://httpbin.org/ get", parameters: ["foo": "bar"]) .validate(statusCode: 200..<300) .validate(contentType: ["application/json"]) .response { (_, _, _, error) in print(error) }
  34. public func latestDaily(completionHandler: (LatestDailyNews?) -> Void) -> Request { return

    manager.request(DailyRouter.LastestDaily) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } } public func dailyNews(forDate date: NSDate, completionHandler: (DailyNews?) -> Void) -> Request { return manager.request(DailyRouter.DailyNews(forDate: date)) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } } public func news(newsId: Int, completionHandler: (News?) -> Void) -> Request { return manager.request(DailyRouter.News(newsId: newsId)) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } }
  35. public func latestDaily(completionHandler: (LatestDailyNews?) -> Void) -> Request { return

    manager.request(DailyRouter.LastestDaily) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } } public func dailyNews(forDate date: NSDate, completionHandler: (DailyNews?) -> Void) -> Request { return manager.request(DailyRouter.DailyNews(forDate: date)) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } } public func news(newsId: Int, completionHandler: (News?) -> Void) -> Request { return manager.request(DailyRouter.News(newsId: newsId)) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } }
  36. public func latestDaily(completionHandler: (LatestDailyNews?) -> Void) -> Request { return

    manager.request(DailyRouter.LastestDaily) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } } public func dailyNews(forDate date: NSDate, completionHandler: (DailyNews?) -> Void) -> Request { return manager.request(DailyRouter.DailyNews(forDate: date)) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } } public func news(newsId: Int, completionHandler: (News?) -> Void) -> Request { return manager.request(URLRequest) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } }
  37. private final func request<T: Decodable where T == T.DecodedType>(URLRequest: URLRequestConvertible,

    completionHandler: T? -> Void) -> Request { return manager.request(URLRequest) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } }
  38. public func latestDaily(completionHandler: (LatestDailyNews?) -> Void) -> Request { return

    request(DailyRouter.LastestDaily, completionHandler: completionHandler) } public func dailyNews(forDate date: NSDate, completionHandler: (DailyNews?) -> Void) -> Request { return request(DailyRouter.DailyNews(forDate: date), completionHandler: completionHandler) } public func news(newsId: Int, completionHandler: (News?) -> Void) -> Request { return request(DailyRouter.News(newsId: newsId), completionHandler: completionHandler) } private final func request<T: Decodable where T == T.DecodedType>(URLRequest: URLRequestConvertible, completionHandler: T? -> Void) -> Request { return manager.request(URLRequest) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } } private final func request<T: Decodable where T == T.DecodedType>(URLRequest: URLRequestConvertible, completionHandler: T? -> Void) -> Request { return manager.request(URLRequest) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } }
  39. public final func longComments(newsId: Int, commentsHandler: (Comments?) -> Void) ->

    Request { return request(DailyRouter.LongComments(newsId: newsId), completionHandler: commentsHandler) } private final func request<T: Decodable where T == T.DecodedType>(URLRequest: URLRequestConvertible, completionHandler: T? -> Void) -> Request { return manager .request(URLRequest) .responseJSON { (request, response, JSON, error) in completionHandler(JSON >>- decode) } }
  40. private final func request<T: Decodable where T == T.DecodedType>(URLRequest: URLRequestConvertible,

    completionHandler: T -> Void) -> Request { return manager .request(URLRequest) .responseJSON { (request, response, JSON, error) in let decoded: T? = JSON >>- decode if let decoded = decoded { completionHandler(decoded) } } Handlers take non-nil values only public final func longComments(newsId: Int, commentsHandler: (Comments) -> Void) -> Request { return request(DailyRouter.LongComments(newsId: newsId), completionHandler: commentsHandler) }
  41. class DailyAPISpecs: QuickSpec { override func spec() { let timeout:

    NSTimeInterval = 20 let api = DailyAPI(userAgent: "SwiftDailySpec") it("loads daily news for a date") { var daily: Daily? = nil let date = NSDate.dateFromString("20150525", format: "yyyyMMdd")! api.daily(forDate: date) { dailyFromAPI in daily = dailyFromAPI } expect(daily).toEventuallyNot(beNil(), timeout: timeout) expect(daily!.date).toEventually(equal(date), timeout: timeout) expect(daily!.news).toEventuallyNot(beEmpty(), timeout: timeout) } } }
  42. class DailyAPISpecs: QuickSpec { override func spec() { let timeout:

    NSTimeInterval = 20 let api = DailyAPI(userAgent: "SwiftDailySpec") it("loads daily news for a date") { var daily: Daily? = nil let date = NSDate.dateFromString("20150525", format: "yyyyMMdd")! api.daily(forDate: date) { dailyFromAPI in daily = dailyFromAPI } expect(daily).toEventuallyNot(beNil(), timeout: timeout) expect(daily!.date).toEventually(equal(date), timeout: timeout) expect(daily!.news).toEventuallyNot(beEmpty(), timeout: timeout) } } }
  43. public class NewsMetaObject: Object { dynamic public var newsId: Int

    = 0 dynamic public var title: String = "" } public class DailyObject: Object { dynamic public var dateHash: Int = 0 dynamic public var date: NSDate = NSDate() public let news = List<NewsMetaObject>() }
  44. public class DailyObject: Object { dynamic public var dateHash: Int

    = 0 dynamic public var date: NSDate = NSDate() public let news = List<NewsMetaObject>() override public static func primaryKey() -> String? { return "dateHash" } override public static func indexedProperties() -> [String] { return ["date", "dateHash"] } }
  45. public class DailyRealmStore { let realm = defaultRealm() public func

    dailyAtDate(date: NSDate) -> DailyObject? { let results = realm.objects(DailyObject).filter("dateHash == \ (date.hash)") return results.first } public func newsWithId(newsId: Int) -> NewsObject? { let results = realm.objects(NewsObject).filter("newsId == \(newsId)") return results.first } }
  46. extension RealmDailyTableViewController { override func viewDidLoad() { super.viewDidLoad() token =

    defaultRealm().addNotificationBlock { (_, _) in self.tableView.reloadData() } } }
  47. Swift 1.2 -> 2.0 • API is better typed, better

    named • guard • @testable
  48. @testable import SwiftDailyAPI class Xcode7PlusSwift2: QuickSpec { override func spec()

    { describe("Xcode7 + Swift 2") { it("is AWESOME!") { Daily(date: NSDate(), news: []) } } } } @testable
  49. public struct Daily { public let date: NSDate public let

    news: [NewsMeta] } extension Daily { public init(_ latestDaily: LatestDaily) { date = latestDaily.date news = latestDaily.news } } extension Daily: Decodable { private static func create(date: NSDate)(news: [NewsMeta]) -> Daily { return Daily(date: date, news: news) } public static func decode(j: JSON) -> Decoded<Daily> { return Daily.create <^> ((j <| "date") >>- toNSDate(DailyConstants.dateFormat)) <*> j <|| "stories" } }
  50. Swift 2.0 -> 1.2 override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)

    { if segue.identifier == "showNews/Realm" { guard let indexPath = tableView.indexPathForSelectedRow else { return } guard let newsMeta = newsMetaAtIndexPath(indexPath) else { return } guard let vc = segue.destinationViewController as? RealmNewsViewController else { return } vc.store = store vc.newsId = newsMeta.newsId } }
  51. Swift 2.0 -> 1.2 override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)

    { if segue.identifier == "showNews/Realm" { if let indexPath = tableView.indexPathForSelectedRow(), let newsMeta = newsMetaAtIndexPath(indexPath), let vc = segue.destinationViewController as? RealmNewsViewController { vc.store = store vc.newsId = newsMeta.newsId } } }