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

Building Real-Time Apps in Swift

Building Real-Time Apps in Swift

Learn why and how to move on from simple data fetching to live queries, continuity and push notifications on iOS. In this talk we'll discuss: - Pros and Cons of moving away from RESTful data feeds and building real-time applications - Tracking changes to your data live using CloudKit - Building seamless experiences by including Handoff and Push Notifications - Sending events in real-time across multiple platforms using Swift

Mateusz Stawecki

September 07, 2016
Tweet

Other Decks in Programming

Transcript

  1. Source: http://www.tapsmart.com/tips-and-tricks/ios-9-pull-to-refresh-your-mail-inbox-on-ipad/ “Pull to Refresh” Expectation: There may be new

    content User’s Responsibility: Trigger refresh. User’s Action: Scroll to where the content is expected to appear.
  2. Better Expectation: Content is always up to date. It shouldn't

    be the user's responsibility to refresh the state of the app.
  3. Cost of faking real-time The ECB app checked for match

    data changes every 30 sec. “I can’t see the player’s stats after I see the goal happen on TV.”
  4. Cost of faking real-time A note-taking app, which updates data

    only on “app open” or “page open” or “return to screen”. “I’ve just added a note on my Mac, why isn’t it showing up on my phone?”
  5. Cost of faking real-time “If the Notes/Safari/etc. app syncs live

    between my devices, why doesn’t app X?”
  6. Ways of doing real-time • Push Notifications and CloudKit Subscriptions:

    background and foreground APNs is Apple exclusive • Live TCP Connection, Live Queries: mostly foreground
  7. Things I won’t go into detail • WebRTC - browser-focused

    set of APIs and protocols for real-time communication like: video/ audio chat, file sharing. • Messaging protocols
  8. Push Notifications api.push.apple.com Provider or App Server Client App device-token

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {…}
  9. Push Notifications api.push.apple.com HTTP POST /3/device/<device-token> Provider or App Server

    Client App device-token Push Certificate func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {…}
  10. Push Notifications • Sample payload: (size limit - 4KB, 2KB

    old API) More at: https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html { "aps": { "alert": "New Message!", "badge": 1, "sound": "default", "content-available": 1, "category": "msg" }, "from": "Matt" }
  11. Push Notifications • Receiving: func application(_ application: UIApplication, didReceiveRemoteNotification userInfo:

    [NSObject : AnyObject]) { if let aps = userInfo["aps"] as? NSDictionary { if let alert = aps["alert"] as? NSDictionary { if let body = alert["body"] as? NSString { print(body) } } else if let alert = aps["alert"] as? NSString { print(alert) } } }
  12. Push Notifications are “cheap”: There is no charge for using

    APNs. • APNs Provider API is available to developers • AWS SNS: $1 per million pushes • Supports broadcast topics • SDK available in multiple languages • Don’t abuse the alerts and silent pushes, but don’t be afraid to use them! • Allow people to opt-in/out
  13. Check out this one weird old trick for making millions

    on the AppStore! Viral App now slide to open
  14. Utilise the push payload in the app: Don’t just send

    alerts. Propagate your UI using push data. { "aps": { "alert": "How's SwiftConf?" }, "msgs": [ { "from": "0044790123456", "ts": 1472841930959, "body": "How's SwiftConf?", "uuid": "816325" } ] }
  15. Send more than “content-available”: You have 4KB of space, this

    should be enough for any metadata associated with the content. { "aps": { "content-available": 1 }, "notes": [ { "id": "2", "title": "Protein Waffles", "ts": "1472842680080", "rev": "3r1sQ", "thumb": “https://th.img.com/2.jpg“, } ] } Loading…
  16. Send more than “content-available”: You have 4KB of space, this

    should be enough for any metadata associated with the content. { "aps": { "content-available": 1 }, "notes": [ { "id": "2", "title": "Protein Waffles", "ts": "1472842680080", "rev": "3r1sQ", "thumb": “https://th.img.com/2.jpg“, } ] }
  17. Push & Pull: Pre-fill with push data, pull data feed

    to validate. { "aps": { … }, "notes": [ { "id": "2", "title": "Protein Waffles", "ts": "1472842680080", "rev": "3r1sQ" } ] } 1. Push Received 2. Show placeholders 3. Load Feed [ { "id": "2", "title": "Protein Waffles", "text": "…", "ts": "1472842680080", "rev": "3r1sQ" }, { "id": "1", "title": "Vegan Cake", "text": "…", "ts": "1461231040080", "rev": “z8iF0" } ] 4. Update UI with Feed
  18. Push & Pull • When Push fails, use standard app

    events. • Trigger with Handoff (NSUserActivity) • If Push Notifications are required anyway, why add sockets on top?
  19. Sync Issues (merging two data sources): • Use IDs and

    Time-stamps • Keep deleted records, but mark as deleted. • See: “Vesper Sync Diary” by Brent Simmons Push & Pull
  20. Consider sending partial data: With 4KB you can experiment with

    sending POIs, partial text content, prefetched URLs, ratings, even thumbnails. 2422 bytes Base64 1814 bytes binary
  21. Checkout changes to Notifications in iOS10 • UserNotifications.framework & UNNotification

    class • Notification Service Extension - handle payload
 before notification is shown, download media. • Notification Content Extension
 - custom notification UI Source:
 Advanced Notifications - WWDC 2016 - Session 708
  22. Use SNS topics to mix between APNs, SQS and more:

    Simplify your backend code by making it transport-agnostic. Message SNS Topic APNs SQS Queue HTTP Endpoint WebSocket Service E-mail Service iOS Device
  23. • Code sample - Registering Device Token with AWS: func

    application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let deviceTokenString: String = (deviceToken.description as String) .trimmingCharacters(in: CharacterSet(charactersIn: "<>")) .replacingOccurrences(of: " ", with: "") as String let sns = AWSSNS.default() let request = AWSSNSCreatePlatformEndpointInput() request?.token = deviceTokenString request?.platformApplicationArn = "app:arn:.." sns.createPlatformEndpoint(request!) .continue(with: AWSExecutor.mainThread(), with: { (task: AWSTask!) -> AnyObject! in if task.error != nil { print("Error: \(task.error)") } else { let createEndpointResponse = task.result! as AWSSNSCreateEndpointResponse print("endpointArn: \(createEndpointResponse.endpointArn)") } return nil }) }
  24. • Code sample - Broadcast to topic with AWS SNS:

    let sns = AWSSNS.default() let request = AWSSNSPublishInput() request?.message = "Hello Users!" request?.topicArn = "arn:aws:sns:us-west-2:0123456:all-users" sns.publish(request!) .continue(with: AWSExecutor.mainThread(), with: { (task: AWSTask!) -> AnyObject! in if task.error != nil { print("Error: \(task.error)") } return nil })
  25. CloudKit • iOS framework for accessing an iCloud-managed database system.

    • Supports: client, server to server, browser. • Simple real-time capabilities with Subscriptions
  26. CloudKit • Sample usage - Adding a Record: let db

    = CKContainer.default().publicCloudDatabase let matchID = CKRecordID(recordName: "1") let place = CKRecord(recordType: "Match", recordID: matchID) place.setObject("John Smith vs Amy Bloggs", forKey: "title") place.setObject("YES", forKey: "live") place.setObject("0:0", forKey: "score") db.save(place) { savedRecord, error in // handle errors: print(error.debugDescription) }
  27. CloudKit • Sample usage - Fetching a Record: let db

    = CKContainer.default().publicCloudDatabase let matchID = CKRecordID(recordName: "1") db.fetch(withRecordID: matchID) { match, error in print(match.debugDescription) self.showScore(title: match?.value(forKey: "title") as! String, score: match?.value(forKey: "score") as! String) }
  28. CloudKit • Creating a Subscription: let db = CKContainer.default().publicCloudDatabase let

    predicate = NSPredicate(format:"live = %@", "YES") let subscription = CKQuerySubscription(recordType: "Match", predicate: predicate, options: .firesOnRecordUpdate) let notification = CKNotificationInfo() notification.alertLocalizationKey = "MATCH_UPDATE" notification.alertLocalizationArgs = ["title", "score"] notification.desiredKeys = ["title", "score"] notification.soundName = "default" subscription.notificationInfo = notification db.save(subscription, completionHandler: { (result, error) -> Void in if error != nil { print(error!.localizedDescription) } })
  29. CloudKit • Receiving Subscription notification: func application(_ application: UIApplication, didReceiveRemoteNotification

    userInfo: [NSObject : AnyObject]) { let notification = CKNotification(fromRemoteNotificationDictionary: userInfo as! [String : NSObject]) if notification.notificationType == .query, let result = notification as? CKQueryNotification { // Fill in data from notification: (PUSH) vc.showScore(title: (result.recordFields?["title"] as! String)+"*", score: result.recordFields?["score"] as! String) // Fetch full record based on ID: (& PULL) let recordID = result.recordID vc.loadScore(recordId: recordID!) } }
  30. CloudKit Subscriptions: • Registered per user’s iCloud account
 (unlike a

    “live query” which is usually active during runtime) • No need to manage push credentials or tokens • App logic can focus on Record operations • Support “content-available” and categories
  31. • CloudKit Best Practices
 WWDC 2016 - Session 231 •

    Introduction to Notifications
 WWDC 2016 - Session 707 • Advanced Notifications
 WWDC 2016 - Session 708 • What's New in the Apple Push Notification Service
 WWDC 2016 - Session 724
  32. Challenges in Swift Cross-platform changes everything! • For non-iOS, leverage

    services:
 Google PubSub, AWS SQS+SNS, PubNub, Firebase, Pusher • Server-side Swift’s HTTP libraries open up a world of services regardless of SDK support.
  33. Live Queries let messagesQuery = Message.query()? .whereKey("roomName", equalTo: "general") ParseLiveQuery.Client()

    .subscribe( messagesQuery ) .handle(Event.Created) { _, message in self.printMessage(message) } https://github.com/ParsePlatform/ParseLiveQuery-iOS-OSX
  34. Challenges in Swift Cross-platform code reuse: • decouple your code

    • use dependency injection • make sure your models are serialisable • prove your concept works on the server first Source: http://www.johndcook.com/blog/2011/07/19/you-wanted-banana/
  35. Architecture Challenges DynamoDB ContentRenderer func renderHTML(node:ContentNode, session:AppSession) -> String {

    } WebAppSession : AppSession LocalAppSession : AppSession var sessionCookie = String() RxSwift SNS FileManager Server iOS func userSettings() func userSettings()
  36. Push @ Tapjet Dropbox File Change Fetch Delta Sync Task

    List folder, enqueue import tasks, write to node graph Enque Sync Tasks Generate new Snapshot Write to asset graph Update Preview App Download processed files
  37. Push @ Tapjet Dropbox File Change Fetch Delta Sync Task

    List folder, enqueue import tasks, write to node graph Enque Sync Tasks Generate new Snapshot Write to asset graph Update Preview App Download processed files web-hook MQ long polling Subscription: task status Silent push: snapshot ID and metadata Subscription: asset status backend device
  38. Push @ Tapjet Latency Interval
 (worst case) Web-hook 250ms 5s

    MQ polling 80ms 5s Subscription (server) 150ms 5s Silent push 500ms 5s Subscription (remote) 350ms 5s Total: 1.3s 25s Comparing push with time-based checks