安定したチャットを実現するための アプリとAPI設計

Ca8210ff0ece2bb6f9fff5fd0770ea64?s=47 Muukii
August 31, 2018

安定したチャットを実現するための アプリとAPI設計

SNS系アプリにおけるコミュニケーション部分で欠かせないチャット機能についてお話します。
エウレカが展開するPairs, Couplesでもチャット機能はサービスの重要な要素として開発に取り組んでいます。
チャット機能は、期待される動作のレベルが高く、同時に様々な例外が起こりえます。
突き詰めるとなかなか大変なプロジェクトです。
最近ではFirebaseやチャットに特化したSaaSなどが多く立ち上がってきており、技術的にリアルタイム性を高く保つことのハードルが下がりつつあります。
またUIの実装ではMessageKitなどの高品質・高機能なライブラリも登場しています。
これらのSaaSやライブラリを使っている方、そうでない方にもきっとどこかで役立つ話をします。

Ca8210ff0ece2bb6f9fff5fd0770ea64?s=128

Muukii

August 31, 2018
Tweet

Transcript

  1. 2.

    ☕ ⌚ About Me • Muukii <Hiroshi Kimura> • iOS

    Engineer at eureka, Inc. • Pairs Global Team • GitHub : @muukii
  2. 3.
  3. 5.

    4PVUI,PSFB Japan Taiwan No.1 2017 release No.1 !5 1BJSTʹ͍ͭͯ ల։ࠃ

    ̐ͭͷϓϥοτϑΥʔϜ CONFIDENTIAL INFORMATION: Not for Public Distribution - Do Not Copy
  4. 6.
  5. 8.

    Կ͔ͱνϟοτΛ࣮૷͖ͯͨ͠ ͜Ε·Ͱ࣮૷͖ͯͨ͠νϟοτ • 2013೥ Pairs ೔ຊ൛ using CoreData (΋͏͜ͷίʔυ͸ফ͞Ε͍ͯΔ) •

    2014೥ Couples ࣮૷ using CoreData • 2017೥ Pairs άϩʔόϧ൛ using Realm • ϝοηʔδͷӬଓԽख๏Ͱ͸CoreDataͱRealmͷ྆ํͷܦݧ͕͋Δ͕ɺ
 Realmͷ΄͏͕ύϑΥʔϚϯενϡʔχϯά͕ߦ͍΍͔ͬͨ͢Πϝʔδ͕͋Δɻ
  6. 11.

    ૹ৴ νϟοτʹظ଴͞ΕΔಈ࡞ • ૬खʹϝοηʔδ͕ಧ͘ • ૹ৴։࢝࣌఺Ͱਧ͖ग़͠ද͕ࣔߦΘΕΔ & ૹ৴தͷεςʔλεʹͳΔ • ૹ৴ʹࣦഊͨ͠ϝοηʔδ͕࢒Δ

    & ࣗಈͰ࠶ૹ͞ΕΔ & ௨৴ঢ়ଶͷ؂ࢹ • ૹ৴ϘλϯԡԼ௚ޙʹΞϓϦΛดͯ͡΋ૹ৴͞ΕΔ • όοΫάϥ΢ϯυૹ৴࣌ʹࣦഊͨ͠ΒϢʔβʔʹࣦഊͨ͜͠ͱΛ఻͑Δ • ૹ৴͢Δલͷϝοηʔδ͸ࣗಈతʹԼॻ͖ͱͯ͠อଘ͞Ε͍ͯΔ
  7. 12.

    Ӿཡ ʢड৴ʣ • ૹ৴͞Εͨॱ൪௨Γʹදࣔ͞ΕΔ • ΦϑϥΠϯͰӾཡՄೳ͕๬·͍͠ (ࠓճͷ࿩͸ΦϑϥΠϯ࣌ରԠ͕ର৅) • ड৴ͨ͠ϝοηʔδ͸ӬଓԽ͞ΕɺεϜʔζʹաڈͷϝοηʔδΛݟΔ͜ͱ͕Ͱ͖Δ •

    աڈͷϝοηʔδΛݟ͍ͯΔͱ͖΋৽ணϝοηʔδʹؾ͚ͮΔΑ͏ʹͯ͋͛͠Δ • ಧ͍ͨΒҰ൪Լ·ͰεΫϩʔϧ͢Δͱ͔ • ը໘ͷԼͷํʹ৽ணϝοηʔδ͕͋Γ·͢ͱ͍͏දࣔΛ͚ͭΔͱ͔ νϟοτʹظ଴͞ΕΔಈ࡞
  8. 15.

    εϨουͷҠಈΛ؆୯ʹ͢Δ Realm Tips extension Realm { public func detached() throws

    -> Realm { return try Realm(configuration: configuration) } } DispatchQueue.global().async { try! mainRealm.detached().write { // Write } }
  9. 16.

    ϝοηʔδͷऔಘ • ϝοηʔδͷऔಘํ๏͸Offset, LimitͷΑ͏ͳAPI͸ద੾Ͱ͸ͳ͍ɻ • Ϩίʔυ͕ͲΜͲΜ௥Ճ͞Ε͍ͯ͘ͷͰoffset͸ৗʹҠಈ͍ͯ͘͠ • TwitterAPI΍SlackAPIͷΑ͏ʹɺRangeͰऔಘͰ͖Δ࢓૊Έ͕๬·͍͠ɻ • Timeline

    Pagination • νϟοτʹݶΒͣɺසൟʹϨίʔυ͕ࠩ͠ࠐ·ΕΔσʔλΛऔಘ͢Δ৔߹ʹ༗ޮ • https://developer.twitter.com/en/docs/tweets/timelines/guides/working-with-timelines.html • https://api.slack.com/methods/channels.history
  10. 19.

    Fetch First Page 1 2 3 4 5 6 7

    8 9 10 offset : 0 limit : 5 Offset Based Pagination 1 2 3 4 5
  11. 20.

    1 2 3 4 5 6 7 8 9 10

    offset : 5 limit : 5 Offset Based Pagination Fetch Next Page 6 7 8 9 10 1 2 3 4 5
  12. 21.

    1 2 3 4 5 6 7 8 9 10

    Offset Based Pagination The Problem Case
  13. 22.

    1 2 3 4 5 6 7 8 9 10

    offset : 0 limit : 5 Offset Based Pagination Fetch First Page 1 2 3 4 5
  14. 23.

    Inserted New Records 1 2 3 4 5 6 7

    8 9 10 offset : 0 limit : 5 1' 2' Offset Based Pagination 1 2 3 4 5
  15. 24.

    offset : 5 limit : 5 1 2 3 4

    5 6 7 8 9 10 1' 2' Offset Based Pagination 1 2 3 4 5 4 5 6 7 8 Fetch Next Page
  16. 25.

    1 2 3 4 5 offset : 5 limit :

    5 1 2 3 4 5 6 7 8 9 10 1' 2' 4 5 6 7 8 ⚠ Duplicated Offset Based Pagination Fetch Next Page
  17. 26.

    Timeline Pagination • औಘ͍ͨ͠σʔλͷൣғΛࢦఆͨ͠औಘํ๏ • σʔλ͕࣋ͭϢχʔΫIDΛ΋ͱʹൣғΛܾఆ͢Δ • ྫ͑͹ • ID͕

    1... ͷ΋ͷΛ10݅ • ID͕ ...1 ͷ΋ͷΛ10݅ • ID͕ 1...100 ͷ΋ͷΛ10݅ • औಘ͢ΔσʔλͷॏෳΛ࠷খݶʹ཈͑ɺαʔόʔɾΫϥΠΞϯτؒͷࠩ෼Λऔಘ͢Δ͜ͱ͕ग़དྷΔ
  18. 27.

    ਎ۙͳͱ͜ΖͰ͸ SlackAPI • Slack (https://api.slack.com/methods/channels.history) • Fetch Messages • Parameters

    • count • number : 10 (Optional, default=100) • latest • timestamp : "1481196383.000292" (Optional, default=now) • oldest • timestamp : "1481196383.000292" (Optional, default=0) Timeline Pagination
  19. 28.

    Timeline Pagination The schema for message using in Slack {

    "type": "message", "ts": "1358546515.000008", "user": "XXXXXXX", "client_msg_id": "d20e760f-456b-4ba0-a8fe-df2935694ee2", "text": "Hello" }
  20. 31.

    1 2 3 4 5 6 7 8 9 10

    Sorted by timestamp Older Newer
  21. 32.

    Fetch First Chunk Timeline Pagination 1 2 3 4 5

    6 7 8 9 10 latest : null oldest : null count : 5 1 2 3 4 5
  22. 33.

    Fetch Next Chunk Timeline Pagination 1 2 3 4 5

    6 7 8 9 10 1 2 3 4 5 "10" "11" "12" "13" "15" timestamp
  23. 34.

    Fetch Next Chunk Timeline Pagination 1 2 3 4 5

    6 7 8 9 10 1 2 3 4 5 latest : oldest : null count : 5 "10" 6 7 8 9 10
  24. 36.

    Fetch First Chunk Timeline Pagination 1 2 3 4 5

    6 7 8 9 10 latest : null oldest : null count : 5 1 2 3 4 5
  25. 38.

    Inserted New Records Timeline Pagination 1 2 3 4 5

    6 7 8 9 10 1 2 3 4 1' 2' 5 "10" "11" "12" "13" "15" timestamp
  26. 39.

    Fetch Next Chunk Timeline Pagination 1 2 3 4 5

    6 7 8 9 10 1 2 3 4 5 1' 2' latest : oldest : null count : 5 "10" 6 7 8 9 10
  27. 40.

    Fetch Latest Items Timeline Pagination 1 2 3 4 5

    6 7 8 9 10 2 3 4 5 1' 2' 6 7 8 9 10 1 "10" "11" "12" "13" "15" timestamp
  28. 41.

    Fetch Latest Items Timeline Pagination 1 2 3 4 5

    6 7 8 9 10 2 3 4 5 1' 2' 6 7 8 9 10 1 latest : null oldest : count : 5 "15" 1' 2'
  29. 42.

    If many items inserted Timeline Pagination 1 2 3 4

    5 2 3 4 5 1' 2' 1 3' 4' 5' 6' 7' 8' 9' 10' "10" "11" "12" "13" "15" timestamp
  30. 43.

    Timeline Pagination 1 2 3 4 5 2 3 4

    5 1' 2' 1 3' 4' 5' 6' 7' 8' 9' 10' latest : null oldest : count : 5 "15" 1' 2' 3' 4' 5' First, fetch latest items
  31. 44.

    Timeline Pagination 1 2 3 4 5 2 3 4

    5 1' 2' 1 3' 4' 5' 6' 7' 8' 9' 10' 1' 2' 3' 4' 5' ⚠ Missing Range Detect item missing range
  32. 45.

    Timeline Pagination 1 2 3 4 5 2 3 4

    5 1' 2' 1 3' 4' 5' 6' 7' 8' 9' 10' 1' 2' 3' 4' 5' "15" "24" "28" "31" Fetch items in missing range using oldest and latest params.
  33. 46.

    Timeline Pagination 1 2 3 4 5 2 3 4

    5 1' 2' 1 3' 4' 5' 6' 7' 8' 9' 10' 1' 2' 3' 4' 5' latest : oldest : count : 5 "15" "24" 6' 7' 8' 9' 10' Fetch completed
  34. 47.

    Data Synchronization • ͜ͷ࢓૊ΈͰαʔόʔͱͷσʔλಉظ͕Մೳʹ • latestͱoldest΍MissingRangeͷύϥϝʔλ͸ΞϓϦͰอ؅͓ͯ͘͠ͱྑ͍ • ΫϥΠΞϯτͰ͸͜ͷऔಘAPIͰऔΕͨσʔλΛਖ਼ͱͯ͠ॲཧ͍ͯ͘͠ͷ͕Φεεϝ • ଞͷॲཧͰDBΛߋ৽ͯ͠΋Α͍͕ɺऔಘॲཧͰ੔ཧ͞ΕΔΠϝʔδ

    • ͜ͷॲཧΛ౔୆ͱͯ͠WebSocket΍NotificationͰड৴ͷτϦΨʔʹ͢ΔͳͲͷϦΞϧλΠϜԽͷΞϓ ϩʔν͕ߦ͑Δ • Ծʹιέοτ௨৴్͕੾Εͨͱͯ͠΋ɺ҆ఆతʹσʔλΛऔಘ͍͚ͯ͠Δ Timeline Pagination
  35. 53.

    • ૹ৴։࢝࣌ʹϝοηʔδ͕දࣔ͞ΕΔ (ૹ৴த) • ✅ ૹ৴੒ޭ • ૹ৴׬ྃͷදࣔΛߦ͏ • ૹ৴ࣦഊ

    • ૹ৴ࣦഊͷදࣔΛߦ͏ • ΞϓϦͷλεΫΛ੾ΒΕͯ΋εςʔλεΛอ؅͢ΔͨΊૹ৴։࢝࣌఺Ͱϝοηʔδ͸ӬଓԽ͢Δ ϝοηʔδͷૹ৴
  36. 54.

    ϝοηʔδΦϒδΣΫτͷεΩʔϚ struct Message { enum State { case draft case

    sending case pendingUpdate case delivered case failed } var sentTime: Date // PrimaryKey var state: State var text: String var clientIdentifier: String } ϝοηʔδͷૹ৴
  37. 55.

    ૹ৴ঢ়ଶͷදݱ struct Message { enum State { case draft case

    sending case pendingUpdate case delivered case failed } var sentTime: Date // PrimaryKey var state: State var text: String var clientIdentifier: String } ϝοηʔδͷૹ৴
  38. 62.

    struct Message { enum State { case draft case sending

    case pendingUpdate case delivered case failed } var sentTime: Date // PrimaryKey var state: State var text: String var clientIdentifier: String } ૹ৴͢ΔϝοηʔδΛτϥοΩϯά͢Δ
  39. 63.

    Ұ࿈ͷྲྀΕ • ϝοηʔδΛState.draftͰ࡞੒͢Δ client_msg_idΛੜ੒͠෇༩͢Δ • ϝοηʔδΛૹ৴͢Δ State.sending • ૹ৴׬ྃ State.pendingUpdate

    • ड৴APIͰऔಘͰ͖ͨϝοηʔδ͕࣋ͭclient_msg_idͰΫϥΠΞϯτଆͷϝοηʔδΛݕࡧ͢Δ • ݟ͔ͭͬͨϝοηʔδΛ State.deliveredʹมߋ͍ͯ͘͠ • State.pendingUpdate -> State.deliverd • State.failed -> State.delivered ૹ৴͢ΔϝοηʔδΛτϥοΩϯά͢Δ
  40. 68.

    • UITableViewͱൺֱͯ͠έʔεʹ߹ΘͤͨॊೈͳରԠ͕Մೳ • ηύϨʔλ͕ແ͍ • UICollectionViewLayoutͰϨΠΞ΢τΛΧελϚΠζ • ଟ͘ͷ৔߹͸UICollectionViewFlowLayoutͰ΍͍͚ͬͯΔ • ϨΠΞ΢τͷ࣮૷͸೉͍͕͠ɺΑΓڧྗͳϨΠΞ΢τΛ࡞Δ͜ͱ͕ग़དྷΔ͔΋͠Εͳ͍

    • FlowLayoutΑΓߴ଎ͳϨΠΞ΢τΛ࡞ΔɺͳͲ • UICollectionViewLayoutAttributesʹΑΔCellͷίϯτϩʔϧ • σʔλͷදࣔϩδοΫͱUIϩδοΫΛ؅ཧ͠΍͘͢ग़དྷΔ͔΋͠Εͳ͍? • UICollectionReusableView.preferredLayoutAttributesFitting(_:) UICollectionView
  41. 70.

    Loading Messages • ӬଓԽ͍ͯ͠Δ͢΂ͯͷϝοηʔδΛදࣔ͢ΔͷͰ͸ͳ͘ɺ࠷৽ͷϝοηʔδ͔Βগͮͭ͠ CollectionViewʹ௥Ճ͍ͯ͘͠ • ॳظදࣔύϑΥʔϚϯεͷͨΊ • ྫ͑͹ɺ࠷ॳ͸20݅දࣔ •

    ϝοηʔδड৴ɾૹ৴Ͱn݅௥Ճ • աڈͷϝοηʔδͷಡΈࠐΈͰ20݅௥Ճ • DB΁ͷΫΤϦ͸දࣔ݅਺Λ૿΍͢͝ͱʹ࣮ߦ͢ΔͷͰ͸ͳ͘ɺશ݅औಘͯ͠ArraySliceΛ࡞ΓɺͦΕ Λ࢖ͬͯCollectionViewʹදࣔΛ͍ͯ͘͠ Using UICollectionView
  42. 72.

    by MessageKit extension UICollectionView { public func scrollToBottom(animated: Bool =

    false) { let collectionViewContentHeight = collectionViewLayout.collectionViewContentSize.height performBatchUpdates(nil) { _ in self.scrollRectToVisible( CGRect( x: 0, y: collectionViewContentHeight - 1, width: 1, height: 1 ), animated: animated ) } } } Scroll to bottom in UICollectionView
  43. 74.
  44. 75.
  45. 77.
  46. 79.
  47. 80.
  48. 82.

    ✅ ͜Ε͕͏·͍͘͘ͱ Inverted UICollectionView • Scroll to bottom͸Scroll to topʹͳָͬͯʹͳΔ

    • աڈͷϝοηʔδಡΈࠐΈ࣌ʹεΫϩʔϧҐஔΛҡ࣋͢Δॲཧ͕ෆཁʹͳΔ
  49. 83.

    ⚠ ஫ҙ఺ Inverted UICollectionView • ΞΠςϜ͕গͳ͍࣌ʹը໘্෦ʹදࣔ͢Δ͜ͱ͕೉͘͠ͳΔ • εςʔλεόʔͷλοϓʹΑΓScroll to top͕ߦΘΕΔͷͰٯʹ͢Δඞཁ͕͋Δ

    • ͦͷ··ͩͱεςʔλεόʔΛλοϓͰ࠷৽ͷϝοηʔδ·ͰඈΜͰ͠·͏ • contentInsetͷઃఆ͕͢΂ͯٯ • OS͕ࣗಈͰઃఆͯ͘͠ΕΔ஋͸͢΂ͯແޮԽͯ͠खಈͰઃఆ͢Δ (safeAreaͳͲʣ
  50. 86.
  51. 88.

    Implementing Growing Text View is too hard • ਓੜͰҰ൪ਏ͍ࢥ͍Λͨ͠UI •

    ΩʔϘʔυͷ্෦ʹுΓ෇͍ͨςΩετೖྗUI • վߦʹԠͯ͡৳ॖ • ৳ͼ͍͚ͯͩ͘ͳΒ؆୯͕ͩɺ৳ॖʹ্ݶΛઃ͚Δͱɺٸʹ࣮૷͕೉͘͠ ͳΔ • ݁ߏݹ͍͚ͲOSS͋Γ·͢ • https://github.com/muukii/NextGrowingTextView • iOS Messagesͷ࣮૷ઃܭͱಉ͡ (ࠓ͸Θ͔Βͳ͍) • ΠϚυΩͳϞμϯͳ࣮૷͕ؾʹͳ͍ͬͯΔͱ͜Ζ It will work better with muukii/NextGrowingTextView.
  52. 89.

    ͓ΘΓʹ • νϟοτ͸SNSܥΞϓϦʹ͓͍ͯ࠷΋ॏཁͳػೳͰ͋Γɺ࣮૷͸೉͍͠৔໘͕ଟʑ͋Δ • ೉͘͠ߟ͑͗͢ΔͱɺͲΜͲΜ೉͘͠ͳ͍ͬͯ͘ • σʔλઃܭ΍UIઃܭʹ͓͍ͯॊೈͳΞΠσΞ͕ٻΊΒΕΔ • ࠓճൃදͨ͠಺༰ʹՃ͑ɺϦΞϧλΠϜԽͷΞϓϩʔν࣍ୈͰ·ͨঢ়گ͸มΘͬͯ͘Δ͸ͣ •

    ࠷ۙͰ͸MessageKit (UI) ΍ ChatKit(SaaS)ͳͲ͕ొ৔͖͓ͯͯ͠Γɺར༻͢Δͷ΋ྑ͍͠ɺ࣮ݱํ๏ͷ ࢀߟʹ΋ͳΔ • νϟοτΛ࡞Ζ͏ͱࢥ͍ͬͯΔਓɺνϟοτͱಆ͖ͬͯͨਓͱҰॹʹ࿩Λ͠·͠ΐ͏ 1
  53. 90.