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

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

Muukii
August 31, 2018

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

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

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