JetForMe
(Rick M)
1
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.
ole
(Ole Begemann)
2
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
JetForMe
(Rick M)
3
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!
mayoff
(Rob Mayoff)
4
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
JetForMe
(Rick M)
5
Unfortunately, that doesn’t let me specify that key path to SwiftUI (afaik).
mayoff
(Rob Mayoff)
6
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