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

オフライン編集もできる複雑なデータ構造を端末間で同期するために / Offline edito...

cc4966
September 19, 2021

オフライン編集もできる複雑なデータ構造を端末間で同期するために / Offline editor and online data sync for complex structures

iOSDC Japan 2021の発表資料です。
2021/09/19 14:50〜 Track D レギュラートーク(40分)
https://fortee.jp/iosdc-japan-2021/proposal/de41bc2f-f910-46ee-be0d-1d7dedb89649

皆さんも開発をしていて端末間のデータ同期システムが必要になったことはありませんか。
この発表ではオフライン編集可能なテキストエディタのバックエンドとして作った、テキストファイル同期システムの設計やその構成、開発環境についてお話しします。
同期システムはiOS / Android / WebだけではなくWindowsなどのデスクトップOSへの展開も見据え、iCloudに依らない実装が必要でした。また作成するアプリの特性上、同期の仕組みをユーザーに十分説明できるように同期の仕組みを全て実装することにしました。

- サーバー構成と開発環境(今回はAzure Functionsを中心に設計したのでその話)
- 採用したシステムとFirestoreを使った場合の比較
- 複雑な系において、アプリのCIにもDockerなどでサーバーを立てるメリット
- オフライン編集を前提とした同期のためのデータベース設計のコツ(ローカル・サーバー両方)
- iOSのファイルアプリに同期対象のファイルを露出させる方法などプラットフォーム固有の話

cc4966

September 19, 2021
Tweet

More Decks by cc4966

Other Decks in Programming

Transcript

  1. ࣗݾ঺հ SPLVSPLV w SPLVSPLV 5XJUUFS!@  w ීஈ΍ͬͯΔ͜ͱ w "1*΍%#ઃܭɾ࣮૷

    w J04"OESPJEΞϓϦ։ൃ w Ϩίϝϯυ΍σʔλ෼ੳ w झຯͰ5"5&EJUPSͱ͍͏ॎॻ͖ͷςΩετΤσΟλΛ։ൃͯ͠Δ 
  2. എܠ ΦϑϥΠϯฤू΋Ͱ͖Δෳࡶͳσʔλߏ଄Λ୺຤ؒͰಉظ͢ΔͨΊʹ w എܠ w झຯͰॎॻ͖ͷςΩετΤσΟλΛ։ൃ w J04"OESPJE 8JOEPXT NBD04

    6CVOUVͰఏڙ w খઆͳͲͷࣥචʹ࢖ΘΕΔ͜ͱ͕ଟ͍ w ύιίϯͱεϚϗ྆ํͰॻ͍͍ͯΔਓ΋ଟ͍ 
  3. ΦϑϥΠϯฤूΛڐ༰͢Δ ؆୯ͳέʔε w ҰͭͷςΩετɺҰͭͷ୺຤͔Βฤूɺෳ਺ͷ୺຤͔ΒӾཡ w औΓ͏Δૢ࡞ʢӾཡଆʣ w ςΩετΛӾཡ͢ΔʢαʔόʔΩϟογϡΛ։͘ʣ w ಉظલ͸ӾཡͰ͖ͳ͍

    w ςΩετΛಉظ͢Δ w ςΩετΛαʔόʔ͔Βऔಘͯ͠αʔόʔΩϟογϡʹอଘ͢Δ ΦϑϥΠϯͰ΋࠷ޙʹऔಘ ͨ͠ςΩετ͕ӾཡͰ͖Δ 
  4. ΦϑϥΠϯฤूΛڐ༰͢Δ ؆୯ͳέʔε w ҰͭͷςΩετɺෳ਺ͷ୺຤͔Βฤूɺෳ਺ͷ୺຤͔ΒӾཡ w ςΩετΛಉظ͢Δ w ϩʔΧϧͱαʔόʔ͕ಉҰˠԿ΋͠ͳ͍ w ϩʔΧϧͷΈมߋ͋Γˠαʔόʔʹ্ॻ͖͢Δ

    w αʔόʔͷΈมߋ͋ΓˠϩʔΧϧʹ্ॻ͖͢Δ w ϩʔΧϧͱαʔόʔ૒ํʹมߋ͋ΓˠίϯϑϦΫτΛղܾ͢Δ  "1*ͰԿΛ্ॻ͖͢Δ͔Λ ໌ࣔͤ͞Δͱҙਤ͠ͳ͍ ্ॻ͖Λ๷͛Δ
  5. ΦϑϥΠϯฤूΛڐ༰͢Δ ؆୯ͳέʔε w ෳ਺ͷςΩετɺෳ਺ͷ୺຤͔Βฤूɺෳ਺ͷ୺຤͔ΒӾཡ w ෳ਺ͷςΩετΛѻ͏͜ͱͰ૿͑Δૢ࡞ w ςΩετΛϩʔΧϧͰ࡞੒͢Δ w ςΩετΛαʔόʔʹ࡞੒͢Δ

    w ςΩετΛϩʔΧϧͰ࡟আ͢Δ w ςΩετΛαʔόʔ͔Β࡟আ͢Δ ௨৴ࣦഊ࣌ɺ࠶ૹ৴ͷલʹॲཧ͕ ੒ޭ͍͔ͯͨ͠Ͳ͏͔໰͍߹ΘͤΔ खஈ΋ඞཁʹͳΔ 
 ʢϩʔΧϧ*%Λอଘ͢ΔͳͲʣ 
  6. ίϯϑϦΫτղফͱͦͷϙϦγʔ ίϯϑϦΫτͷ໰୊ w ίϯϑϦΫτͱ͸ w ͋Δ୺຤ͱผͷ୺຤ͷ྆ํͰಉ͡ςΩετΛฤूͨ͠ঢ়ଶ w ਖ਼͠͞͸Ϣʔβʔ͔͠ධՁͰ͖ͳ͍ w ༷ʑͳέʔε͕͋Δ

    w ྫʣ΋ͬͱྑ͍ςΩετΛࢥ͍͍ͭͯॻ͍ͨˠ্ॻ͖͢Δ΂͖ w ྫʣهԱʹͳ͍ੲͷΦϑϥΠϯมߋ͕࢒ͬͯͨˠഁغ͢Δ΂͖ 
  7. ίϯϑϦΫτղফͱͦͷϙϦγʔ ίϯϑϦΫτͰͱΕΔखஈ w ϢʔβʔʹͲ͏͢Δ͔બ͹ͤΔ w ͋ͱͰฉ͘ɺ͋ͱͰ֬ೝͰ͖ΔΑ͏ʹ͢Δ w ςΩετͷมߋཤྺΛ಺෦Ͱอ࣋͢Δ w ྫʣ

    w όʔδϣϯόʔδϣϯ͔Βߋ৽ w όʔδϣϯόʔδϣϯ͔Βߋ৽ ಉ͡όʔδϣϯΛߋ৽͢Δཤྺ ʹίϯϑϦΫτ 
  8. σΟϨΫτϦߏ଄ ෳࡶͳσʔλߏ଄ w σΟϨΫτϦߏ଄ w ϧʔτʹϑΥϧμ΋͘͠͸ςΩετ͕ଘࡏ͢Δ w ϑΥϧμͷதʹ΋ϑΥϧμ΋͘͠͸ςΩετ͕ଘࡏ͢Δ w ϑΥϧμ΋͘͠͸ςΩετ͸λΠτϧΛ࣋ͭ

    w ςΩετ͸࡟আ͞ΕΔͱΰϛശʹೖΔʢ෮ݩͰ͖Δʣ w ʢʴϑΥϧμ಺ͷςΩετͱϑΥϧμͷදࣔॱং͸อଘ͞ΕΔʣ ࿩࿩ͷΑ͏ͳ έʔε͕ଟ͍ 
  9. σΟϨΫτϦߏ଄ ෳࡶͳσʔλߏ଄ w ΞΠςϜͷ৘ใ w *% w λΠτϧ w ਌ͷΞΠςϜ*%

    w खલͷΞΠςϜ*% w ςΩετ*% w ςΩετͷ৘ใ w *% w ຊจ
  10. σΟϨΫτϦߏ଄ ෳࡶͳσʔλߏ଄ͷૢ࡞ w औΓ͏Δૢ࡞ w ϑΥϧμ΍ςΩετΛϦωʔϜ͢ΔʢλΠτϧͷมߋʣ w ϑΥϧμ΍ςΩετΛ࡞੒͢Δ w ϑΥϧμ΍ςΩετΛ࡟আ͢ΔʢϑΥϧμͷத਎΋࡟আ͞ΕΔʣ

    w ϑΥϧμ΍ςΩετΛҠಈ͢ΔʢॴଐϑΥϧμͷมߋʣ w ϑΥϧμ΍ςΩετΛฒͼସ͑ΔʢϑΥϧμ಺ͷදࣔॱংͷมߋʣ ςΩετຊจͷߋ৽ʹ ૬౰͠ɺσʔλߏ଄ͷ ੔߹ੑʹ͸Өڹ͠ͳ͍ 
  11. ։ൃ؀ڥʹ͍ͭͯ 5*14 w 9DPEFCFUBͰBTZODBXBJUΛར༻ͯ͠௨৴ؚΉςετΛॻ͍ͨ w ςετΫϥε͚ͩ!BWBJMBCMF J04  Ͱׅͬͨ w

    ςετͷEFQMPZNFOUUBSHFUΛม͑ΔͰ΋ྑ͍ w ςετ͚ͩͰ΋BTZODBXBJUΛ࢖͏͜ͱͰίʔυྔΛ͔ͳΓ࡟ݮͰ͖ͨ 
  12. 9DPEFͷBTZODBXBJUར༻ #FGPSF func testαʔόʔͰςΩετΛ࡞੒_αʔόʔͰcontent࡟আ_ಉظ_ςΩετΛ෮ݩ_ಉظͷࡍʹطʹαʔόʔͰtext࡟আࡁΈ_৽͍ۭ͠ͷςΩετͱͯ͠ಉظ͞ΕΔ() throws { let awaitExpectation = expectation(description:

    "await") try Auth.auth().signOut() Auth.auth().signInAnonymously(completion: { result, error in do { guard result != nil else { XCTFail(error?.localizedDescription ?? #function) awaitExpectation.fulfill() return } try self.changeServer(operation: .createDocumentRoot(title: nil, text: "text")) { result in switch result { case .success(let root): try self.changeServer(operation: .removeContent(contentId: root.id)) { result in switch result { case .success: try self.sync(dbQueue: self.dbQueue) { result in switch result { case .success: let serverTextId1 = root.textId! let localTextUuid1 = try self.dbQueue.selectTextUuid(id: serverTextId1) try self.changeServer(operation: .removeText(textId: serverTextId1)) { result in switch result { case .success: let local = try self.changeLocalOffline(dbQueue: self.dbQueue, operation: .restoreDocumentRoot(title: "local", textUuid: localTextUuid1)) try self.sync(dbQueue: self.dbQueue) { result in switch result { case .success: let localTextUuid3 = local.textUuid! XCTAssertEqual(localTextUuid1, localTextUuid3) awaitExpectation.fulfill() case .failure(let error): XCTFail(error.localizedDescription) awaitExpectation.fulfill() } } case .failure(let error): XCTFail(error.localizedDescription) awaitExpectation.fulfill() } } case .failure(let error): XCTFail(error.localizedDescription) awaitExpectation.fulfill() } } case .failure(let error): XCTFail(error.localizedDescription) awaitExpectation.fulfill() } } case .failure(let error): XCTFail(error.localizedDescription) awaitExpectation.fulfill() } } } catch { XCTFail(error.localizedDescription) awaitExpectation.fulfill() } }) } 
  13. 9DPEFͷBTZODBXBJUར༻ "GUFS func testαʔόʔͰςΩετΛ࡞੒_αʔόʔͰcontent࡟আ_ಉظ_ςΩετΛ෮ݩ_ಉظͷࡍʹطʹαʔόʔͰtext࡟আࡁΈ_৽͍ۭ͠ͷςΩετͱͯ͠ಉظ͞ΕΔ() async throws { try await signIn()

    let root = try await changeServer(operation: .createDocumentRoot(title: nil, text: "text")) _ = try await changeServer(operation: .removeContent(contentId: root.id)) try await sync(dbQueue: dbQueue) let serverTextId1 = root.textId! let localTextUuid1 = try dbQueue.selectTextUuid(id: serverTextId1) _ = try await changeServer(operation: .removeText(textId: serverTextId1)) let local = try changeLocalOffline(dbQueue: dbQueue, operation: .restoreDocumentRoot(title: "local", textUuid: localTextUuid1)) try await sync(dbQueue: dbQueue) let localTextUuid3 = local.textUuid! XCTAssertEqual(localTextUuid1, localTextUuid3) }