Skip to content

Instantly share code, notes, and snippets.

@krzyzanowskim
Last active November 8, 2024 19:58
Show Gist options
  • Save krzyzanowskim/133ad12e30c3883ad59de6f1156a28a4 to your computer and use it in GitHub Desktop.
Save krzyzanowskim/133ad12e30c3883ad59de6f1156a28a4 to your computer and use it in GitHub Desktop.
FB15439084: NSTextList is Equatable in Swift but not equatable in common sense

NSTextList is Equatable because NSObject is always Equatable, however if anyone expect that may compare two instances of NSTextList that is false. This is because NSTextList.isEqual() do return true value for equal instances. This situation makes impossible to eg. compare NSParagraphStyle instances with testLists property as it will always fail. This situation seems easy fixable and will significanlty improve testability of the codebases (eg. mine)

        do {
            let a = NSTextList(markerFormat: .disc, options: 0) // Equatable
            let b = NSTextList(markerFormat: .disc, options: 0) // Equatable
            let c = a == b        // false
            let co = a.isEqual(b) // false
        }

        do {
            let a = AnyHashable(NSTextList(markerFormat: .disc, options: 0))
            let b = AnyHashable(NSTextList(markerFormat: .disc, options: 0))
            let c = a == b // false
        }
@krzyzanowskim
Copy link
Author

// Compare attributes for the EditAction needs only
// NSTextList is not Equatable properly that braks NSParagraphStyle comparison.
// FB15439084: https://gist.github.com/krzyzanowskim/133ad12e30c3883ad59de6f1156a28a4
// `EquatableTextList` is equatable wrapper used only by the EditAction.
// (This could be workaround with extension to NSTextList but I choose otherwise, to not leak that anywhere else)
final class EquatableTextList: NSTextList {
    init(textList: NSTextList) {
        super.init(markerFormat: textList.markerFormat, options: NSTextList.Options(rawValue: textList.listOptions.rawValue), startingItemNumber: textList.startingItemNumber)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func isEqual(_ object: Any?) -> Bool {
        if super.isEqual(object) {
            return true
        }

        guard let object = object as? NSTextList else {
            return false
        }

        if self.markerFormat == object.markerFormat, self.listOptions == object.listOptions, self.startingItemNumber == object.startingItemNumber {
            return true
        }

        return false
    }

    override var hash: Int {
        var hasher = Hasher()
        hasher.combine(markerFormat)
        hasher.combine(listOptions.rawValue)
        hasher.combine(startingItemNumber)
        return hasher.finalize()
    }
}
import XCTest

private extension NSAttributedString {
    func equatable() -> NSAttributedString {
        let copy = self.mutableCopy() as! NSMutableAttributedString
        copy.makeParagraphEquatable()
        return copy
    }
}

private extension NSMutableAttributedString {
    func makeParagraphEquatable() {
        enumerateAttribute(.paragraphStyle, in: fullRange) { value, range, stop in
            guard let paragraphStyle = value as? NSParagraphStyle else {
                return
            }
            let copy = paragraphStyle.typedMutableCopy()
            copy.textLists = copy.textLists.map {
                EquatableTextList(textList: $0)
            }
            self.addAttribute(.paragraphStyle, value: copy, range: range)
        }
    }
}

/// Compare NSAttributedString
func XCTAssertEqualAttributedString<T>(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) where T : NSAttributedString {
    try XCTAssertEqual(expression1().equatable(), expression2().equatable(), message(), file: file, line: line)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment