diff --git a/apps/ios/Snowbound/Arranging.swift b/apps/ios/Snowbound/Arranging.swift index 123ad7ae4bd9bae9d1c4760b594c849431722c8c..1f2014b2773c8d45fa73d4c742d5fc4a0ada5f84 100644 --- a/apps/ios/Snowbound/Arranging.swift +++ b/apps/ios/Snowbound/Arranging.swift @@ -152,7 +152,7 @@ enum Arranging { reported = moved release() for section in Section.all where section.notebook === notebook && [from, to].contains(section.tab.path) { - section.listed() + section.reloadRows() } if moved == nil { failed("Can’t Move Page") } } @@ -242,8 +242,8 @@ enum Arranging { } if !reciprocal && !reported.isEmpty { done("Move Pages") { undo() } } release() - source.listed() - target.listed() + source.reloadRows() + target.reloadRows() if !succeeded { failed("Can’t Move All Pages", reported.isEmpty ? "Check both sections, then try moving the remaining pages again." @@ -306,14 +306,8 @@ extension Section { /// Puts page `id` where `order` places it, as one edit; false where it isn't listed. func arrange(_ order: [Placed], moving id: String) -> Bool { guard sb_section_arrange(handle, json(order), json([id])) else { return false } - listed() - return true - } - - /// Lists the pages again and tells the views. - func listed() { reloadRows() - NotificationCenter.default.post(name: Self.changed, object: self, userInfo: ["flags": Self.listed]) + return true } } diff --git a/apps/ios/Snowbound/Notebooks.swift b/apps/ios/Snowbound/Notebooks.swift index f5d394b5f306c0d32790f173910c08f2bf7cd158..fa63d3af34b7e6ba431727d9b010340d4dff81a1 100644 --- a/apps/ios/Snowbound/Notebooks.swift +++ b/apps/ios/Snowbound/Notebooks.swift @@ -629,7 +629,7 @@ private struct Weak { weak var value: T? } /// A section open for editing: pages listed and saved through its replica. final class Section { - /// Sent on the main thread with `flags` from `sb_section_poll` after each poll. + /// Sent on the main thread with `flags` from `sb_section_poll`, `listed` once the rows are read again. static let changed = Notification.Name("SectionChanged") static let listed: UInt32 = 1 static let remote: UInt32 = 2 @@ -642,17 +642,18 @@ final class Section { let tab: Tab let handle: OpaquePointer private let token: Int - private(set) var rows: [Row] = [] + private(set) var rows: [Row] private(set) var status = SaveStatus.unknown + private var listing: (invalidated: Bool, completions: [() -> Void])? static var all: [Section] { open.values.compactMap(\.value) } - private init(notebook: Notebook, tab: Tab, handle: OpaquePointer, token: Int) { + private init(notebook: Notebook, tab: Tab, handle: OpaquePointer, token: Int, rows: [Row]) { self.notebook = notebook self.tab = tab self.handle = handle self.token = token - reloadRows() + self.rows = rows } deinit { @@ -671,21 +672,53 @@ final class Section { let token = tokens let pointer = Int(bitPattern: library) let author = Author.name ?? "" - background({ () -> (Int, String?) in + background({ () -> (Int, String?, [Row]) in var error: UnsafeMutablePointer? let handle = sb_section_open( OpaquePointer(bitPattern: pointer), tab.path, author, wakeSection, UInt(token), &error) - return (Int(bitPattern: handle), take(error)) - }) { handle, error in + let rows = handle.flatMap { decode([Row].self, sb_section_pages($0)) } ?? [] + return (Int(bitPattern: handle), take(error), rows) + }) { handle, error, rows in guard let handle = OpaquePointer(bitPattern: handle) else { return done(nil, error) } - let section = Section(notebook: notebook, tab: tab, handle: handle, token: token) + let section = Section(notebook: notebook, tab: tab, handle: handle, token: token, rows: rows) open[token] = Weak(value: section) done(section, nil) + // Wakes before `open[token]` was set went unheard. + section.poll() } } + /// Lists the pages again and tells the views. func reloadRows() { + listing?.invalidated = true rows = decode([Row].self, sb_section_pages(handle)) ?? rows + NotificationCenter.default.post(name: Self.changed, object: self, userInfo: ["flags": Self.listed]) + } + + /// Lists the pages again off the main thread, tells the views, then runs `then`. + private func refreshRows(then: @escaping () -> Void = {}) { + guard listing == nil else { + listing?.invalidated = true + listing?.completions.append(then) + return + } + listing = (false, [then]) + readRows() + } + + private func readRows() { + background({ [self] in decode([Row].self, sb_section_pages(handle)) }) { [self] rows in + // A read begun before a reload or retitle would undo it. + if listing!.invalidated, rows != nil { + listing?.invalidated = false + return readRows() + } + self.rows = rows ?? self.rows + let completions = listing!.completions + listing = nil + NotificationCenter.default.post(name: Self.changed, object: self, userInfo: ["flags": Self.listed]) + for completion in completions { completion() } + } } /// The row listing page or conflict page `id`, and the version when it is one. @@ -701,17 +734,19 @@ final class Section { var status: UInt8 = 0 let flags = sb_section_poll(handle, &status) let changed = SaveStatus(rawValue: status) ?? .unknown - let unchanged = flags == 0 && changed == self.status + if flags & Self.listed != 0 { refreshRows() } + let unlisted = flags & ~Self.listed + let unchanged = unlisted == 0 && changed == self.status self.status = changed - if flags & Self.listed != 0 { reloadRows() } NotificationCenter.default.post( name: Self.changed, object: self, - userInfo: unchanged ? ["flags": flags, "unchanged": true] : ["flags": flags]) + userInfo: unchanged ? ["flags": unlisted, "unchanged": true] : ["flags": unlisted]) } /// A page's title as typed, shown in the list before the section reports it. func retitle(_ id: String, _ title: String) { guard let index = rows.firstIndex(where: { $0.id == id }), rows[index].title != title else { return } + listing?.invalidated = true rows[index].title = title NotificationCenter.default.post(name: Self.changed, object: self, userInfo: ["flags": UInt32(0)]) } @@ -721,7 +756,6 @@ final class Section { let (date, time) = titleDate() let id = take(sb_section_new_page(handle, parent, date, time)) reloadRows() - NotificationCenter.default.post(name: Self.changed, object: self, userInfo: ["flags": Self.listed]) return id } @@ -735,13 +769,13 @@ final class Section { } return true }) { [self] deleted in - release() - reloadRows() - NotificationCenter.default.post(name: Self.changed, object: self, userInfo: ["flags": Self.listed]) - if deleted { done() } - else { - let window = UIApplication.shared.connectedScenes.lazy.compactMap { ($0 as? UIWindowScene)?.keyWindow }.first - window?.rootViewController?.alert("Can’t Delete", "Some pages couldn’t be deleted. Try again.") + refreshRows { + release() + if deleted { done() } + else { + let window = UIApplication.shared.connectedScenes.lazy.compactMap { ($0 as? UIWindowScene)?.keyWindow }.first + window?.rootViewController?.alert("Can’t Delete", "Some pages couldn’t be deleted. Try again.") + } } } }