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

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

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Muukii Muukii
August 31, 2018

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

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

Avatar for Muukii

Muukii

August 31, 2018
Tweet

More Decks by Muukii

Other Decks in Programming

Transcript

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

    Engineer at eureka, Inc. • Pairs Global Team • GitHub : @muukii
  2. 4PVUI,PSFB Japan Taiwan No.1 2017 release No.1 !5 1BJSTʹ͍ͭͯ ల։ࠃ

    ̐ͭͷϓϥοτϑΥʔϜ CONFIDENTIAL INFORMATION: Not for Public Distribution - Do Not Copy
  3. Կ͔ͱνϟοτΛ࣮૷͖ͯͨ͠ ͜Ε·Ͱ࣮૷͖ͯͨ͠νϟοτ • 2013೥ Pairs ೔ຊ൛ using CoreData (΋͏͜ͷίʔυ͸ফ͞Ε͍ͯΔ) •

    2014೥ Couples ࣮૷ using CoreData • 2017೥ Pairs άϩʔόϧ൛ using Realm • ϝοηʔδͷӬଓԽख๏Ͱ͸CoreDataͱRealmͷ྆ํͷܦݧ͕͋Δ͕ɺ
 Realmͷ΄͏͕ύϑΥʔϚϯενϡʔχϯά͕ߦ͍΍͔ͬͨ͢Πϝʔδ͕͋Δɻ
  4. ૹ৴ νϟοτʹظ଴͞ΕΔಈ࡞ • ૬खʹϝοηʔδ͕ಧ͘ • ૹ৴։࢝࣌఺Ͱਧ͖ग़͠ද͕ࣔߦΘΕΔ & ૹ৴தͷεςʔλεʹͳΔ • ૹ৴ʹࣦഊͨ͠ϝοηʔδ͕࢒Δ

    & ࣗಈͰ࠶ૹ͞ΕΔ & ௨৴ঢ়ଶͷ؂ࢹ • ૹ৴ϘλϯԡԼ௚ޙʹΞϓϦΛดͯ͡΋ૹ৴͞ΕΔ • όοΫάϥ΢ϯυૹ৴࣌ʹࣦഊͨ͠ΒϢʔβʔʹࣦഊͨ͜͠ͱΛ఻͑Δ • ૹ৴͢Δલͷϝοηʔδ͸ࣗಈతʹԼॻ͖ͱͯ͠อଘ͞Ε͍ͯΔ
  5. Ӿཡ ʢड৴ʣ • ૹ৴͞Εͨॱ൪௨Γʹදࣔ͞ΕΔ • ΦϑϥΠϯͰӾཡՄೳ͕๬·͍͠ (ࠓճͷ࿩͸ΦϑϥΠϯ࣌ରԠ͕ର৅) • ड৴ͨ͠ϝοηʔδ͸ӬଓԽ͞ΕɺεϜʔζʹաڈͷϝοηʔδΛݟΔ͜ͱ͕Ͱ͖Δ •

    աڈͷϝοηʔδΛݟ͍ͯΔͱ͖΋৽ணϝοηʔδʹؾ͚ͮΔΑ͏ʹͯ͋͛͠Δ • ಧ͍ͨΒҰ൪Լ·ͰεΫϩʔϧ͢Δͱ͔ • ը໘ͷԼͷํʹ৽ணϝοηʔδ͕͋Γ·͢ͱ͍͏දࣔΛ͚ͭΔͱ͔ νϟοτʹظ଴͞ΕΔಈ࡞
  6. εϨουͷҠಈΛ؆୯ʹ͢Δ Realm Tips extension Realm { public func detached() throws

    -> Realm { return try Realm(configuration: configuration) } } DispatchQueue.global().async { try! mainRealm.detached().write { // Write } }
  7. ϝοηʔδͷऔಘ • ϝοηʔδͷऔಘํ๏͸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
  8. 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
  9. 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
  10. 1 2 3 4 5 6 7 8 9 10

    Offset Based Pagination The Problem Case
  11. 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
  12. 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
  13. 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
  14. 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
  15. Timeline Pagination • औಘ͍ͨ͠σʔλͷൣғΛࢦఆͨ͠औಘํ๏ • σʔλ͕࣋ͭϢχʔΫIDΛ΋ͱʹൣғΛܾఆ͢Δ • ྫ͑͹ • ID͕

    1... ͷ΋ͷΛ10݅ • ID͕ ...1 ͷ΋ͷΛ10݅ • ID͕ 1...100 ͷ΋ͷΛ10݅ • औಘ͢ΔσʔλͷॏෳΛ࠷খݶʹ཈͑ɺαʔόʔɾΫϥΠΞϯτؒͷࠩ෼Λऔಘ͢Δ͜ͱ͕ग़དྷΔ
  16. ਎ۙͳͱ͜ΖͰ͸ 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
  17. 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" }
  18. 1 2 3 4 5 6 7 8 9 10

    Sorted by timestamp Older Newer
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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'
  27. 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
  28. 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
  29. 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
  30. 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.
  31. 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
  32. Data Synchronization • ͜ͷ࢓૊ΈͰαʔόʔͱͷσʔλಉظ͕Մೳʹ • latestͱoldest΍MissingRangeͷύϥϝʔλ͸ΞϓϦͰอ؅͓ͯ͘͠ͱྑ͍ • ΫϥΠΞϯτͰ͸͜ͷऔಘAPIͰऔΕͨσʔλΛਖ਼ͱͯ͠ॲཧ͍ͯ͘͠ͷ͕Φεεϝ • ଞͷॲཧͰDBΛߋ৽ͯ͠΋Α͍͕ɺऔಘॲཧͰ੔ཧ͞ΕΔΠϝʔδ

    • ͜ͷॲཧΛ౔୆ͱͯ͠WebSocket΍NotificationͰड৴ͷτϦΨʔʹ͢ΔͳͲͷϦΞϧλΠϜԽͷΞϓ ϩʔν͕ߦ͑Δ • Ծʹιέοτ௨৴్͕੾Εͨͱͯ͠΋ɺ҆ఆతʹσʔλΛऔಘ͍͚ͯ͠Δ Timeline Pagination
  33. • ૹ৴։࢝࣌ʹϝοηʔδ͕දࣔ͞ΕΔ (ૹ৴த) • ✅ ૹ৴੒ޭ • ૹ৴׬ྃͷදࣔΛߦ͏ • ૹ৴ࣦഊ

    • ૹ৴ࣦഊͷදࣔΛߦ͏ • ΞϓϦͷλεΫΛ੾ΒΕͯ΋εςʔλεΛอ؅͢ΔͨΊૹ৴։࢝࣌఺Ͱϝοηʔδ͸ӬଓԽ͢Δ ϝοηʔδͷૹ৴
  34. ϝοηʔδΦϒδΣΫτͷεΩʔϚ 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 } ϝοηʔδͷૹ৴
  35. ૹ৴ঢ়ଶͷදݱ 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 } ϝοηʔδͷૹ৴
  36. 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. Ұ࿈ͷྲྀΕ • ϝοηʔδΛState.draftͰ࡞੒͢Δ client_msg_idΛੜ੒͠෇༩͢Δ • ϝοηʔδΛૹ৴͢Δ State.sending • ૹ৴׬ྃ State.pendingUpdate

    • ड৴APIͰऔಘͰ͖ͨϝοηʔδ͕࣋ͭclient_msg_idͰΫϥΠΞϯτଆͷϝοηʔδΛݕࡧ͢Δ • ݟ͔ͭͬͨϝοηʔδΛ State.deliveredʹมߋ͍ͯ͘͠ • State.pendingUpdate -> State.deliverd • State.failed -> State.delivered ૹ৴͢ΔϝοηʔδΛτϥοΩϯά͢Δ
  38. • UITableViewͱൺֱͯ͠έʔεʹ߹ΘͤͨॊೈͳରԠ͕Մೳ • ηύϨʔλ͕ແ͍ • UICollectionViewLayoutͰϨΠΞ΢τΛΧελϚΠζ • ଟ͘ͷ৔߹͸UICollectionViewFlowLayoutͰ΍͍͚ͬͯΔ • ϨΠΞ΢τͷ࣮૷͸೉͍͕͠ɺΑΓڧྗͳϨΠΞ΢τΛ࡞Δ͜ͱ͕ग़དྷΔ͔΋͠Εͳ͍

    • FlowLayoutΑΓߴ଎ͳϨΠΞ΢τΛ࡞ΔɺͳͲ • UICollectionViewLayoutAttributesʹΑΔCellͷίϯτϩʔϧ • σʔλͷදࣔϩδοΫͱUIϩδοΫΛ؅ཧ͠΍͘͢ग़དྷΔ͔΋͠Εͳ͍? • UICollectionReusableView.preferredLayoutAttributesFitting(_:) UICollectionView
  39. Loading Messages • ӬଓԽ͍ͯ͠Δ͢΂ͯͷϝοηʔδΛදࣔ͢ΔͷͰ͸ͳ͘ɺ࠷৽ͷϝοηʔδ͔Βগͮͭ͠ CollectionViewʹ௥Ճ͍ͯ͘͠ • ॳظදࣔύϑΥʔϚϯεͷͨΊ • ྫ͑͹ɺ࠷ॳ͸20݅දࣔ •

    ϝοηʔδड৴ɾૹ৴Ͱn݅௥Ճ • աڈͷϝοηʔδͷಡΈࠐΈͰ20݅௥Ճ • DB΁ͷΫΤϦ͸දࣔ݅਺Λ૿΍͢͝ͱʹ࣮ߦ͢ΔͷͰ͸ͳ͘ɺશ݅औಘͯ͠ArraySliceΛ࡞ΓɺͦΕ Λ࢖ͬͯCollectionViewʹදࣔΛ͍ͯ͘͠ Using UICollectionView
  40. 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
  41. ✅ ͜Ε͕͏·͍͘͘ͱ Inverted UICollectionView • Scroll to bottom͸Scroll to topʹͳָͬͯʹͳΔ

    • աڈͷϝοηʔδಡΈࠐΈ࣌ʹεΫϩʔϧҐஔΛҡ࣋͢Δॲཧ͕ෆཁʹͳΔ
  42. ⚠ ஫ҙ఺ Inverted UICollectionView • ΞΠςϜ͕গͳ͍࣌ʹը໘্෦ʹදࣔ͢Δ͜ͱ͕೉͘͠ͳΔ • εςʔλεόʔͷλοϓʹΑΓScroll to top͕ߦΘΕΔͷͰٯʹ͢Δඞཁ͕͋Δ

    • ͦͷ··ͩͱεςʔλεόʔΛλοϓͰ࠷৽ͷϝοηʔδ·ͰඈΜͰ͠·͏ • contentInsetͷઃఆ͕͢΂ͯٯ • OS͕ࣗಈͰઃఆͯ͘͠ΕΔ஋͸͢΂ͯແޮԽͯ͠खಈͰઃఆ͢Δ (safeAreaͳͲʣ
  43. Implementing Growing Text View is too hard • ਓੜͰҰ൪ਏ͍ࢥ͍Λͨ͠UI •

    ΩʔϘʔυͷ্෦ʹுΓ෇͍ͨςΩετೖྗUI • վߦʹԠͯ͡৳ॖ • ৳ͼ͍͚ͯͩ͘ͳΒ؆୯͕ͩɺ৳ॖʹ্ݶΛઃ͚Δͱɺٸʹ࣮૷͕೉͘͠ ͳΔ • ݁ߏݹ͍͚ͲOSS͋Γ·͢ • https://github.com/muukii/NextGrowingTextView • iOS Messagesͷ࣮૷ઃܭͱಉ͡ (ࠓ͸Θ͔Βͳ͍) • ΠϚυΩͳϞμϯͳ࣮૷͕ؾʹͳ͍ͬͯΔͱ͜Ζ It will work better with muukii/NextGrowingTextView.
  44. ͓ΘΓʹ • νϟοτ͸SNSܥΞϓϦʹ͓͍ͯ࠷΋ॏཁͳػೳͰ͋Γɺ࣮૷͸೉͍͠৔໘͕ଟʑ͋Δ • ೉͘͠ߟ͑͗͢ΔͱɺͲΜͲΜ೉͘͠ͳ͍ͬͯ͘ • σʔλઃܭ΍UIઃܭʹ͓͍ͯॊೈͳΞΠσΞ͕ٻΊΒΕΔ • ࠓճൃදͨ͠಺༰ʹՃ͑ɺϦΞϧλΠϜԽͷΞϓϩʔν࣍ୈͰ·ͨঢ়گ͸มΘͬͯ͘Δ͸ͣ •

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