From 9dd966f6390a24ebd82afd2c7ccca6c7846fcd22 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 1 Jan 2023 15:12:31 -0500 Subject: [PATCH] Fix duplicate saved instances not being uniqued correctly --- Tusker.xcodeproj/project.pbxproj | 4 ++ Tusker/Extensions/Array+Uniques.swift | 33 ++++++++++---- .../Explore/ExploreViewController.swift | 2 +- .../Main/MainSidebarViewController.swift | 2 +- TuskerTests/ArrayUniqueTests.swift | 45 +++++++++++++++++++ 5 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 TuskerTests/ArrayUniqueTests.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 57e9ecff..ea94b6e0 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -288,6 +288,7 @@ D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */; }; D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; }; D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; }; + D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; }; D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */; }; D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */; }; D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D12B57292D5B2C00D528E1 /* StatusActionAccountListViewController.swift */; }; @@ -672,6 +673,7 @@ D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainComposeTextView.swift; sourceTree = ""; }; D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = ""; }; D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = ""; }; + D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = ""; }; D6D12B2E2925D66500D528E1 /* TimelineGapCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineGapCollectionViewCell.swift; sourceTree = ""; }; D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCollectionViewCell.swift; sourceTree = ""; }; D6D12B57292D5B2C00D528E1 /* StatusActionAccountListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActionAccountListViewController.swift; sourceTree = ""; }; @@ -1485,6 +1487,7 @@ D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */, D6114E1627F8BB210080E273 /* VersionTests.swift */, D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */, + D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */, D6D4DDE6212518A200E1C4BB /* Info.plist */, ); path = TuskerTests; @@ -2136,6 +2139,7 @@ D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */, D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */, D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */, + D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */, D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tusker/Extensions/Array+Uniques.swift b/Tusker/Extensions/Array+Uniques.swift index 29fdc8ae..e2e77eba 100644 --- a/Tusker/Extensions/Array+Uniques.swift +++ b/Tusker/Extensions/Array+Uniques.swift @@ -8,16 +8,31 @@ import Foundation -extension Array where Element: Hashable { - func uniques() -> [Element] { - var buffer = [Element]() - var added = Set() +extension Array { + func uniques(by identify: (Element) -> ID) -> [Element] { + var uniques = Set>() for elem in self { - if !added.contains(elem) { - buffer.append(elem) - added.insert(elem) - } + uniques.insert(Hashed(element: elem, id: identify(elem))) } - return buffer + return uniques.map(\.element) + } +} + +extension Array where Element: Hashable { + func uniques() -> [Element] { + return uniques(by: { $0 }) + } +} + +fileprivate struct Hashed: Hashable { + let element: Element + let id: ID + + static func ==(lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) } } diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index c6064ba8..a8e5a88c 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -216,7 +216,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!) req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)] do { - return try mastodonController.persistentContainer.viewContext.fetch(req).uniques() + return try mastodonController.persistentContainer.viewContext.fetch(req).uniques(by: \.url) } catch { return [] } diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index dab7647d..50e017e6 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -250,7 +250,7 @@ class MainSidebarViewController: UIViewController { let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!) req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)] do { - return try mastodonController.persistentContainer.viewContext.fetch(req).uniques() + return try mastodonController.persistentContainer.viewContext.fetch(req).uniques(by: \.url) } catch { return [] } diff --git a/TuskerTests/ArrayUniqueTests.swift b/TuskerTests/ArrayUniqueTests.swift new file mode 100644 index 00000000..3d6cc23b --- /dev/null +++ b/TuskerTests/ArrayUniqueTests.swift @@ -0,0 +1,45 @@ +// +// ArrayUniqueTests.swift +// TuskerTests +// +// Created by Shadowfacts on 1/1/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import XCTest +@testable import Tusker + +final class ArrayUniqueTests: XCTestCase { + + func testUniquesBy() { + let a = Test(string: "test") + let b = Test(string: "test") + XCTAssertNotEqual(a.id, b.id) + XCTAssertNotEqual(a.hashValue, b.hashValue) + XCTAssertEqual([a, b].uniques(by: \.string), [a]) + } + + class Test: NSObject { + let id = UUID() + let string: String + + init(string: String) { + self.string = string + } + + override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? Self else { + return false + } + return id == other.id && string == other.string + } + + override var hash: Int { + var hasher = Hasher() + hasher.combine(id) + hasher.combine(string) + return hasher.finalize() + } + } + +}