Fix crash when sort and filter change at the same time.

Fix edit table cell during reload
This commit is contained in:
relikd
2020-06-20 12:52:17 +02:00
parent 778f377e42
commit 26f6ea1a9a
3 changed files with 68 additions and 77 deletions

View File

@@ -1111,7 +1111,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements; CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 19; CURRENT_PROJECT_VERSION = 20;
INFOPLIST_FILE = main/Info.plist; INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1130,7 +1130,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements; CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 19; CURRENT_PROJECT_VERSION = 20;
INFOPLIST_FILE = main/Info.plist; INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1149,7 +1149,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements; CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19; CURRENT_PROJECT_VERSION = 20;
INFOPLIST_FILE = GlassVPN/Info.plist; INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN"; PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
@@ -1167,7 +1167,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements; CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19; CURRENT_PROJECT_VERSION = 20;
INFOPLIST_FILE = GlassVPN/Info.plist; INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN"; PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";

View File

@@ -1,24 +1,15 @@
import UIKit import UIKit
protocol FilterPipelineDelegate: AnyObject { protocol FilterPipelineDelegate: AnyObject {
/// Call `reloadData()` on main thread. /// Call `reloadData()`
/// - Warning: This function may be called from a background thread.
func filterPipelineDidReset() func filterPipelineDidReset()
/// Call `safeDeleteRows()`
/// Call `safeDeleteRows()` on main thread.
/// - Warning: This function may be called from a background thread.
func filterPipeline(delete rows: [Int]) func filterPipeline(delete rows: [Int])
/// Call `safeInsertRow()`
/// Call `safeInsertRow()` on main thread.
/// - Warning: This function may be called from a background thread.
func filterPipeline(insert row: Int) func filterPipeline(insert row: Int)
/// Call `safeReloadRow()`
/// Call `safeReloadRow()` on main thread.
/// - Warning: This function may be called from a background thread.
func filterPipeline(update row: Int) func filterPipeline(update row: Int)
/// Call `safeMoveRow()`
/// Call `safeMoveRow()` on main thread.
/// - Warning: This function may be called from a background thread.
func filterPipeline(move oldRow: Int, to newRow: Int) func filterPipeline(move oldRow: Int, to newRow: Int)
} }

View File

@@ -101,7 +101,9 @@ extension GroupedDomainDataSource {
for (i, val) in logs.enumerated() { for (i, val) in logs.enumerated() {
logs[i].options = DomainFilter[val.domain] logs[i].options = DomainFilter[val.domain]
} }
pipeline.reset(dataSource: logs) DispatchQueue.main.sync {
pipeline.reset(dataSource: logs)
}
} }
func syncUpdate(_: SyncUpdate, insert rows: SQLiteRowRange, affects: SyncUpdateEnd) { func syncUpdate(_: SyncUpdate, insert rows: SQLiteRowRange, affects: SyncUpdateEnd) {
@@ -109,17 +111,19 @@ extension GroupedDomainDataSource {
assertionFailure("NotifySyncInsert fired with empty range") assertionFailure("NotifySyncInsert fired with empty range")
return return
} }
cellAnimationsGroup(if: latest.count > 14) DispatchQueue.main.sync {
for x in latest { cellAnimationsGroup(if: latest.count > 14)
if let (i, obj) = pipeline.dataSourceGet(where: { $0.domain == x.domain }) { for x in latest {
pipeline.update(obj + x, at: i) if let (i, obj) = pipeline.dataSourceGet(where: { $0.domain == x.domain }) {
} else { pipeline.update(obj + x, at: i)
var y = x } else {
y.options = DomainFilter[x.domain] var y = x
pipeline.addNew(y) y.options = DomainFilter[x.domain]
pipeline.addNew(y)
}
} }
cellAnimationsCommit()
} }
cellAnimationsCommit()
} }
func syncUpdate(_ sender: SyncUpdate, remove rows: SQLiteRowRange, affects: SyncUpdateEnd) { func syncUpdate(_ sender: SyncUpdate, remove rows: SQLiteRowRange, affects: SyncUpdateEnd) {
@@ -132,21 +136,23 @@ extension GroupedDomainDataSource {
outdated.count > 0 else { outdated.count > 0 else {
return return
} }
cellAnimationsGroup(if: outdated.count > 14) DispatchQueue.main.sync {
var listOfDeletes: [Int] = [] cellAnimationsGroup(if: outdated.count > 14)
for x in outdated { var listOfDeletes: [Int] = []
guard let (i, obj) = pipeline.dataSourceGet(where: { $0.domain == x.domain }) else { for x in outdated {
assertionFailure("Try to remove non-existent element") guard let (i, obj) = pipeline.dataSourceGet(where: { $0.domain == x.domain }) else {
continue // should never happen assertionFailure("Try to remove non-existent element")
} continue // should never happen
if obj.total > x.total { }
pipeline.update(obj - x, at: i) if obj.total > x.total {
} else { pipeline.update(obj - x, at: i)
listOfDeletes.append(i) } else {
listOfDeletes.append(i)
}
} }
pipeline.remove(indices: listOfDeletes.sorted())
cellAnimationsCommit()
} }
pipeline.remove(indices: listOfDeletes.sorted())
cellAnimationsCommit()
} }
func syncUpdate(_ sender: SyncUpdate, partialRemove affectedFQDN: String) { func syncUpdate(_ sender: SyncUpdate, partialRemove affectedFQDN: String) {
@@ -155,17 +161,19 @@ extension GroupedDomainDataSource {
return // does not affect current table return // does not affect current table
} }
let affected = (parent == nil ? affectedParent : affectedFQDN) let affected = (parent == nil ? affectedParent : affectedFQDN)
guard let old = pipeline.dataSourceGet(where: { $0.domain == affected }) else { let updated = AppDB?.dnsLogsGrouped(range: sender.rows, matchingDomain: affected, parentDomain: parent)?.first
// can only happen if delete sheet is open while background sync removed the element DispatchQueue.main.sync {
return guard let old = pipeline.dataSourceGet(where: { $0.domain == affected }) else {
} // can only happen if delete sheet is open while background sync removed the element
if var updated = AppDB?.dnsLogsGrouped(range: sender.rows, matchingDomain: affected, return
parentDomain: parent)?.first { }
assert(old.object.domain == updated.domain) if var updated = updated {
updated.options = DomainFilter[updated.domain] assert(old.object.domain == updated.domain)
pipeline.update(updated, at: old.index) updated.options = DomainFilter[updated.domain]
} else { pipeline.update(updated, at: old.index)
pipeline.remove(indices: [old.index]) } else {
pipeline.remove(indices: [old.index])
}
} }
} }
} }
@@ -180,9 +188,8 @@ extension GroupedDomainDataSource {
extension GroupedDomainDataSource { extension GroupedDomainDataSource {
/// Sets `pipeline.delegate = nil` to disable individual cell animations (update, insert, delete & move). /// Sets `pipeline.delegate = nil` to disable individual cell animations (update, insert, delete & move).
private func cellAnimationsGroup(if condition: Bool = true) { private func cellAnimationsGroup(if condition: Bool = true) {
if condition { pipeline.delegate = nil } if condition || delegate?.tableView.isFrontmost == false {
if pipeline.delegate != nil { pipeline.delegate = nil
onMain { if !$0.isFrontmost { self.pipeline.delegate = nil } }
} }
} }
/// No-Op if cell animations are enabled already. /// No-Op if cell animations are enabled already.
@@ -190,33 +197,26 @@ extension GroupedDomainDataSource {
private func cellAnimationsCommit() { private func cellAnimationsCommit() {
if pipeline.delegate == nil { if pipeline.delegate == nil {
pipeline.delegate = self pipeline.delegate = self
onMain { $0.reloadData() } delegate?.tableView.reloadData()
}
}
/// Perform table view manipulations on main thread.
/// Set `delegate = nil` to disable `tableView` animations.
private func onMain(_ closure: (UITableView) -> Void) {
if Thread.isMainThread {
if let tv = delegate?.tableView { closure(tv) }
} else {
DispatchQueue.main.sync {
if let tv = delegate?.tableView { closure(tv) }
}
} }
} }
// TODO: Collect animations and post them in a single animations block. // TODO: Collect animations and post them in a single animations block.
// This will require enormous work to translate then into a final set. // This will require enormous work to translate them into a final set.
func filterPipelineDidReset() { onMain { $0.reloadData() } } func filterPipelineDidReset() { delegate?.tableView.reloadData() }
func filterPipeline(delete rows: [Int]) { onMain { $0.safeDeleteRows(rows) } } func filterPipeline(delete rows: [Int]) { delegate?.tableView.safeDeleteRows(rows) }
func filterPipeline(insert row: Int) { onMain { $0.safeInsertRow(row, with: .left) } } func filterPipeline(insert row: Int) { delegate?.tableView.safeInsertRow(row, with: .left) }
func filterPipeline(update row: Int) { onMain { $0.safeReloadRow(row) } } func filterPipeline(update row: Int) {
guard let tv = delegate?.tableView else { return }
if !tv.isEditing { tv.safeReloadRow(row) }
else if tv.isFrontmost == true {
delegate?.groupedDomainDataSource(needsUpdate: row)
}
}
func filterPipeline(move oldRow: Int, to newRow: Int) { func filterPipeline(move oldRow: Int, to newRow: Int) {
onMain { delegate?.tableView.safeMoveRow(oldRow, to: newRow)
$0.safeMoveRow(oldRow, to: newRow) if delegate?.tableView.isFrontmost == true {
if $0.isFrontmost { // onMain ensures delegate is set delegate?.groupedDomainDataSource(needsUpdate: newRow)
delegate!.groupedDomainDataSource(needsUpdate: newRow)
}
} }
} }
} }