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

Same language, different platform - searching for synergy between iOS and Vapor

Same language, different platform - searching for synergy between iOS and Vapor

Being able to share code between an iOS app and a backend running server-side Swift was one of the first things I thought about when Swift went open source. It’s been a common argument for choosing a server-side Swift framework ever since, although there’s been little focus on what is actually possible. With this talk I will dive into the current possibilities and limitations for sharing code between iOS and Vapor and I will look ahead and discus how this might change in the future.

Steffen D. Sommer

September 20, 2018
Tweet

More Decks by Steffen D. Sommer

Other Decks in Programming

Transcript

  1. !

  2. • " Head of Vapor Development at Nodes • #

    Background in iOS development • $ Full time Vapor developer since January 2017 • % ~50 customer projects and ~40 packages • & On the hunt for synergy.. @steffendsommer
  3. '

  4. • ( A web framework built in Swift • )

    Official support for SQLite, MySQL, PostgreSQL and Redis • * Built on top of SwiftNIO
  5. import Vapor let app = try Application() let router =

    try app.make(Router.self) router.get("hello") { req in return "Hello, world." } try app.run()
  6. • + Strong, safe and modern language • , Fast

    and has a low memory footprint (aka. -) • ♻ Potential synergy between iOS and Vapor • / Tech awesomeness
  7. • * Modern and Swifty API’s • 0 Great community

    • 1 Growing eco system • 2 Most popular
  8. 3 • 4 Everything is “new” • ( Tooling •

    ⚙ Foundation • 1 Eco system • 6 Resources
  9. –Wikipedia “Synergy is the creation of a whole that is

    greater than the simple sum of its parts.”
  10. " 9

  11. :

  12. ;

  13. # “Can I get an endpoint which return a list

    of subcategories?” “Yes.. it’s ready now” “Wait a sec, that endpoint gives me product categories?” “Oh.. so you want categories to be returned?”
  14. <

  15. • ⏱ To save time (write once, use twice) •

    "9 To share work-load • : To keep code in sync across platforms • ; To share knowledge about implementation • < To hide the data interchange format
  16. =

  17. final class Work: Codable { enum Kind: String, Codable {

    // .. } enum Framework: String, Codable { // .. } var id: Int? var company: String var companyLogoUrl: String var location: String var kind: Kind var framework: Framework var remoteAllowed: Bool var title: String var description: String var externalUrl: String var contactEmail: String var approvedAt: Date? var createdAt: Date? var updatedAt: Date? var deletedAt: Date? init( company: String, companyLogoUrl: String, location: String, kind: Kind, framework: Framework, remoteAllowed: Bool, title: String, description: String, externalUrl: String, contactEmail: String ) { // .. } }
  18. # struct Work: Codable { enum Kind: String, Codable {

    // .. } enum Framework: String, Codable { // .. } var id: Int? var company: String var companyLogoUrl: String var location: String var kind: Kind var framework: Framework var remoteAllowed: Bool var title: String var description: String var externalUrl: String var contactEmail: String var approvedAt: Date? var createdAt: Date? var updatedAt: Date? var deletedAt: Date? }
  19. ♻ public final class Work: Codable { public enum Kind:

    String, Codable { // .. } public enum Framework: String, Codable { // .. } public var id: Int? public var company: String public var companyLogoUrl: String public var location: String public var kind: Kind public var framework: Framework public var remoteAllowed: Bool public var title: String public var description: String public var externalUrl: String public var contactEmail: String public var approvedAt: Date? public var createdAt: Date? public var updatedAt: Date? public var deletedAt: Date? public init( company: String, companyLogoUrl: String, location: String, kind: Kind, framework: Framework, remoteAllowed: Bool, title: String, description: String, externalUrl: String, contactEmail: String ) { // .. } }
  20. enum WorkError: Error { case noWorkAvailable } extension WorkError: AbortError

    { var identifier: String { return "noWork" } var status: HTTPResponseStatus { return .custom(code: 499, reasonPhrase: "No work available") } var reason: String { return "Unfortunately, no work available at this moment." } }
  21. # enum WorkError: Error { case noWork } extension WorkError

    { init?(from statusCode: Int) { switch statusCode { case 499: self = .noWork default: return nil } } }
  22. ♻ enum APIError: Error { case noWorkAvailable var httpCode: UInt

    { switch self { case .noWorkAvailable: return 499 } } init?(from httpCode: UInt) { switch httpCode { case 499: self = .noWorkAvailable default: return nil } } }
  23. ?

  24. public func routes(_ router: Router) throws { let api =

    router.grouped("api") let workAPIController = APIWorkController() api.get("/work", use: workAPIController.workList) api.get("/work", Work.parameter, use: workAPIController.work) } enum EnvironmentKey { enum Project { static let url = "PROJECT_URL" } }
  25. # struct WorkAPIService { enum Environment: String { case production

    = "http://serversideswift.work" } let environment: Environment func work(complete: @escaping ([Work]) -> ()) { sendRequest(to: "\(environment.rawValue)/api/work/") { (work: [Work]) in complete(work) } } func work(for id: Int, complete: @escaping (Work) -> ()) { sendRequest(to: "\(environment.rawValue)/api/work/\(id)") { (work: Work) in complete(work) } } }
  26. ♻ public enum API { public enum Environment: String {

    case production = "http://serversideswift.work" func url(to endpoint: Endpoint) -> String { return self.rawValue + endpoint.path } } public enum Endpoint { case workList case work(Int) var path: String { switch self { case .workList: return "/api/work" case .work(let id): return "/api/work/\(id)" } } } }
  27. final class APIWorkController { func workList(_ req: Request) throws ->

    Future<[Work]> { return Work .query(on: req) .filter(\.approvedAt != nil) .sort(\.approvedAt, .descending) .all() .thenThrowing { items in guard items.count > 0 else { throw WorkError.noWorkAvailable } return items } } func work(_ req: Request) throws -> Future<Work> { return try req.parameters.next(Work.self) } }
  28. # struct WorkAPIService { let environment: API.Environment func work(complete: @escaping

    ([Work]) -> ()) { sendRequest(to: environment.url(to: .workList)) { (work: [Work]) in complete(work) } } func work(for id: Int, complete: @escaping (Work) -> ()) { sendRequest(to: environment.url(to: .work(id))) { (work: Work) in complete(work) } } func sendRequest<C: Codable>(to url: String, complete: @escaping (C) -> ()) { guard let url = URL(string: url) else { return } let task = URLSession.shared.dataTask(with: url) { (data, response, error) in guard let dataResponse = data, let urlResponse = response as? HTTPURLResponse, error == nil else { return } do { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let results = try decoder.decode(C.self, from: dataResponse) complete(results) } catch let parsingError { print("Error", parsingError) } } task.resume() } }
  29. ♻ public protocol HTTPClient { func sendRequest<C: Codable>(to url: String,

    complete: @escaping (C) -> ()) } public struct APIClient { internal let client: HTTPClient internal let environment: API.Environment public init(client: HTTPClient, environment: API.Environment) { self.client = client self.environment = environment } public func work(complete: @escaping ([Work]) -> ()) { client.sendRequest(to: environment.url(to: .workList)) { (work: [Work]) in complete(work) } } public func work(for id: Int, complete: @escaping (Work) -> ()) { client.sendRequest(to: environment.url(to: .work(id))) { (work: Work) in complete(work) } } }
  30. A

  31. • = Models • ⚠ Errors • ✅ Validation •

    ? Endpoints & Environments • C Business logic • D Styling • E Test code (e.g. mocks) • F Frameworks used in both places • ☂ A framework that wraps the endpoints
  32. G • # Copy files from shared repo into your

    project • $ Copy files from shared repo into your project
  33. H • # Add submodule and drag files into project

    • $ Add submodule and re-generate Xcode project file
  34. < • # Use SPM to pull down dependences and

    use this trick* to generate an iOS project. Alternatively use SwiftXcode. • $ Add the shared repo as a dependency * https://www.ralfebert.de/ios-examples/xcode/ios-dependency-management-with-swift-package-manager/
  35. I • # Check in Xcode project on shared repo

    and add the dependency to your Cartfile • $ N/A
  36. • G Copy and paste • H Git submodules •

    < Swift Package Manager • I Carthage • ☕ CocoaPods
  37. • K A change most likely requires platform-specific updates •

    L Both platforms would still have to sync and deploy/release • ☁ Code might depend on something that doesn’t run on the other platform • M Potential overhead in abstracting code that can be shared • N Not great until SPM gets integrated in Xcode 3
  38. O

  39. • < Swift Package Manager integrated in Xcode • P

    Fluent on iOS • $ Vapor on iOS • Q Examples/resources/tools/frameworks • R Swift on Android
  40. • $ https://docs.vapor.codes • S https://discord.gg/vapor • T https://www.serversideswift.info •

    U Tim Condon - Getting Started with Server Side Swift and Vapor (Codemobile 2018) • 9 https://www.serversideswift.work