Trouble with KeyPathComparator and Bool (related: TableColumn)

I'm making a simple SwiftUI app with a sortable table. Everything seems fine until I try to make my table sortable by a Bool property. The model looks like this:

struct ProcessFile : Codable
{
	var	id										=	UUID()
	var label				:	String
	var	plistInfo			:	FileInfo
	var executableInfo		:	FileInfo?
	var executableExists	:	Bool			{ return self.executableInfo != nil }
}

struct FileInfo : Codable
{
	var	path				:	Path
	var	created				:	Date
	var	modified			:	Date
}

When I try to make a KeyPathComparator using executableExists, I get errors:

Model.swift:146:49: error: cannot convert value of type 'KeyPath<ProcessFile, Bool>' to expected argument type 'KeyPath<ProcessFile, Value?>'
    self.tempFiles.sort(using: KeyPathComparator(\ProcessFile.executableExists))
                                                 ^
Model.swift:146:49: note: arguments to generic parameter 'Value' ('Bool' and 'Value?') are expected to be equal
    self.tempFiles.sort(using: KeyPathComparator(\ProcessFile.executableExists))
                                                 ^
Model.swift:146:31: error: generic parameter 'Value' could not be inferred
    self.tempFiles.sort(using: KeyPathComparator(\ProcessFile.executableExists))
                               ^
Foundation.KeyPathComparator:6:12: note: in call to initializer
    public init<Value>(_ keyPath: KeyPath<Compared, Value?>, order: SortOrder = .forward) where Value : Comparable

Relatedly, I get an error if I try to make one of the associated TableColumn views use that Bool property:

ContentView.swift:73:5: error: referencing initializer 'init(_:value:content:)' on 'TableColumn' requires that 'ProcessFile' inherit from 'NSObject'
    TableColumn("􀭉", value: \.executableExists)
SwiftUI.TableColumn:4:11: note: where 'RowValue' = 'ProcessFile'
extension TableColumn where RowValue : NSObject, Sort == SortDescriptor<RowValue>, Label == Text {
          ^
ContentView.swift:73:5: error: referencing initializer 'init(_:value:content:)' on 'TableColumn' requires the types 'KeyPathComparator<ProcessFile>' and 'SortDescriptor<ProcessFile>' be equivalent
    TableColumn("􀭉", value: \.executableExists)
SwiftUI.TableColumn:4:11: note: where 'Sort' = 'KeyPathComparator<ProcessFile>', 'SortDescriptor<RowValue>' = 'SortDescriptor<ProcessFile>'
extension TableColumn where RowValue : NSObject, Sort == SortDescriptor<RowValue>, Label == Text {

I see no reason why I shouldn’t be able to use a Bool type here, and the docs even explicitly show methods for Bool.

I'm guessing this is because that KeyPathComparator initializer requires the key path to point to a Comparable property, and Bool is not Comparable.

2 Likes

Yup, that was it. I guess I couldn't see that because I consider Bool to be comparable. In any case, conforming it to Comparable was trivial, and now it does what I want. Thanks!

Thou shalt not conform thy neighbor's type to thy neighbor's protocol. A Swift community term for this is retroactive conformance and it's bad idea—especially if the type and the protocol are part of the SDK (see the link).

Instead, define a BoolComparator:

struct BoolComparator: SortComparator {
    var order: SortOrder = .forward
    
    func compare(_ lhs: Bool, _ rhs: Bool) -> ComparisonResult {
        return order == .forward ? result(lhs, rhs) : result(rhs, lhs)
    }
    
    private func result(_ lhs: Bool, _ rhs: Bool) -> ComparisonResult {
        if !lhs && rhs { return .orderedAscending }
        if lhs && !rhs { return .orderedDescending }
        return .orderedSame
    }
}

And use it like this:

self.tempFiles.sort(
    using: KeyPathComparator(
        \ProcessFile.executableExists,
        comparator: BoolComparator()
    )
)
1 Like

Unfortunately, that doesn’t let me specify that key path to SwiftUI (afaik).

Do you mean you cannot specify it to TableColumn? TableColumn has initializers that take a SortComparator, such as init(_:value:comparator:content:) (which also takes a KeyPath) and init(_:sortUsing:content:) (which doesn't).

\.executableExists.comparable
public extension Bool {
  /// A way to compare `Bool`s.
  ///
  /// Note: `false` is "less than" `true`.
  enum Comparable: CaseIterable, Swift.Comparable {
    case `false`, `true`
  }

  /// Make a `Bool` `Comparable`, with `false` being "less than" `true`.
  var comparable: Comparable { .init(booleanLiteral: self) }
}

extension Bool.Comparable: ExpressibleByBooleanLiteral {
  public init(booleanLiteral value: Bool) {
    self = value ? .true : .false
  }
}
1 Like