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.

9993186221ec65f6f10db0dc9cff7c07?s=128

Steffen D. Sommer

September 20, 2018
Tweet

Transcript

  1. Same language, different platform Searching for synergy between iOS and

    Vapor SwiftConf 2018
  2. !

  3. • " 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
  4. '

  5. None
  6. “Platform support for all Apple platforms as well as Linux”

  7. None
  8. None
  9. • ( A web framework built in Swift • )

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

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

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

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

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

    greater than the simple sum of its parts.”
  16. None
  17. ☁ # JSON Models Models Errors Errors Validation Validation Endpoints

    Endpoints
  18. ☁ # JSON Models Models Errors Errors Validation Validation Endpoints

    Endpoints
  19. ☁ # ♻ Models Errors Validation Endpoints JSON

  20. None
  21. None
  22. " 9

  23. " 9 ♻

  24. :

  25. None
  26. ;

  27. # “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?”
  28. <

  29. • ⏱ 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
  30. None
  31. =

  32. 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 ) { // .. } }
  33. # 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? }
  34. ♻ 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 ) { // .. } }
  35. 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." } }
  36. # enum WorkError: Error { case noWork } extension WorkError

    { init?(from statusCode: Int) { switch statusCode { case 499: self = .noWork default: return nil } } }
  37. ♻ 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 } } }
  38. ?

  39. 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" } }
  40. # 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) } } }
  41. ♻ 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)" } } } }
  42. 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) } }
  43. # 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() } }
  44. ♻ 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) } } }
  45. A

  46. • = 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
  47. ♻ ☁ # my-app-ios my-app-vapor my-app-shared

  48. None
  49. G • # Copy files from shared repo into your

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

    • $ Add submodule and re-generate Xcode project file
  51. < • # 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/
  52. I • # Check in Xcode project on shared repo

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

    < Swift Package Manager • I Carthage • ☕ CocoaPods
  54. • 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
  55. O

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

    Fluent on iOS • $ Vapor on iOS • Q Examples/resources/tools/frameworks • R Swift on Android
  57. • $ 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
  58. V Any questions? @steffendsommer