got2jam
got2jam

Reputation: 595

Swift - How to use a class type as a parameter / variable

I'm trying to create a somewhat generic function to sort file URLs based the value of an attribute

My goal is to:

1) Pass the URL and some parameters (Including the type) to a function. which will then loop through the file's attributes

2) Add the matching file attribute and the URL to an array of tuples

3) Sort the tuples by the value of the found attribute

4) Return the sorted array and display the items in sorted order

I believe that I need to pass the type of the attribute into the sorting function so I'm able to set it in the tuple since I'm unable to sort with "Any" but I'm unsure of how to do that

I'm okay with passing anything into the sorting function and constructing or deconstructing the value I need in the sorting function since that will be predefined depending upon what action is selected by the user

//Initial implementation to be later tied to IBActions and simplified   

func sortFiles(sortByKeyString : String, fileURLArray : [URL]) -> [URL] 
{

        switch sortByKeyString {
            case "date-created-DESC":
                let fileAttributeKeyString : String = "creationDate"
                let isSortOrderDesc = true
                let objectTypeString : String = NSDate.className()
                let sortedFileURLArray = sortFileArrayByType(fileAttributeKeyString: fileAttributeKeyString, fileURLArray: fileURLArray, type: objectTypeString, isSortOrderDesc : isSortOrderDesc)
                return sortedFileURLArray
            default:
                return fileURLArray
        }
    }

//Generic function to get a files attributes from a URL by requested 
type

    func sortFileArrayByType(fileAttributeKeyString : String, fileURLArray : [URL], type: String, isSortOrderDesc : Bool) -> [URL] {
        let fileManager = FileManager.default
        let attributeToLookFor : FileAttributeKey = FileAttributeKey.init(rawValue: fileAttributeKeyString)
        var tupleArrayWithURLandAttribute : [(url: URL, attribute: *Any*)]? = nil

        for url in fileURLArray {
            do {
                let attributes = try fileManager.attributesOfItem(atPath: url.path)
                for (key, value) in attributes {
                    if key.rawValue == fileAttributeKeyString {
                        tupleArrayWithURLandAttribute?.append((url: url, attribute: value))
                    }

                }
                let sortedTupleArrayWithURLandAttribute = tupleArrayWithURLandAttribute?.sorted(by: { $0.attribute < $1.attribute)})
                // Need to Sort dictionary into array
                return sortedTupleArrayWithURLandAttribute
            } catch {
                return fileURLArray
            }
        }
    }

Upvotes: 1

Views: 3306

Answers (3)

Rob Napier
Rob Napier

Reputation: 299623

What you're looking for here is a way to sort a sequence of URLs by a URLResourceKey (and specifically by the URLResourceValues property related to that key). Unfortunately, URLResourceValues aren't mapped to URLResourceKey in a useful way. But we can fix that with an extension:

extension URLResourceValues {
    static func key<T>(for keyPath: KeyPath<Self, T>) -> URLResourceKey {
        switch keyPath {
        case \Self.creationDate: return .creationDateKey
        // ... Other keys ...
        default: fatalError()
        }
    }
}

And it would be very useful to get a value for a URLResourceValues keyPath:

extension URL {
func resourceValue<T>(for keyPath: KeyPath<URLResourceValues, T?>) throws -> T? {
        return try resourceValues(forKeys: Set([URLResourceValues.key(for: keyPath)]))[keyPath: keyPath]
    }
}

With that, we can build a sorting method based on URLResourceValues (assuming nil is less than other values; you could replace that with throwing for non-existent values):

extension Sequence where Element == URL {
    func sorted<T>(by keyPath: KeyPath<URLResourceValues, T?>) throws -> [URL]
        where ResourceType: Comparable {
            return try self
                .sorted { (lhs, rhs) in
                    guard let lhsValue = try lhs.resourceValue(for: keyPath)
                        else { return true }
                    guard let rhsValue = try rhs.resourceValue(for: keyPath)
                        else { return false }
                    return lhsValue < rhsValue
            }
    }
}

And finally, that can be used by passing a keypath, based on URLResourceValues:

let sortedFiles = try files.sorted(by: \.creationDate)

Upvotes: 0

CRD
CRD

Reputation: 53010

First read Metatype Type in the The Swift Programming Language. Once read continue with the answer.

From that you have learnt that you can declare a function parameter's type to be the type of types (you are allowed to go crosseyed), AKA metatype, and can therefore pass a type to a function. Combine that with generics and Swift's type inference and you could declare your function as:

func sortFileArrayByType<T>(fileAttributeKeyString : String,
                            attributeType : T.Type,
                            fileURLArray : [URL]
                           ) -> [(url: URL, attribute: T)]
                           where T : Comparable

This adds the parameter attributeType whose type is the metatype of T where T will be inferred. For example the metatype String.self could be passed and T will be inferred to be String.

The where clause constrains T so that only types which are Comparable are allowed, this is required to enable the function to do sorting. File attributes can be Date, String and NSNumber valued; unfortunately the latter does not conform to Comparable so you need to add an extension to make it, the following will suffice:

extension NSNumber : Comparable
{
   public static func <(a : NSNumber, b : NSNumber) -> Bool { return a.compare(b) == .orderedAscending }
   public static func ==(a : NSNumber, b : NSNumber) -> Bool { return a.compare(b) == .orderedSame }
}

Within the body of the function you need to declare your array of tuples to have attributes of type T:

var tupleArrayWithURLandAttribute : [(url: URL, attribute: T)] = []

and when you add entries you need to cast the value returned by attributesOfItem to be T:

tupleArrayWithURLandAttribute.append((url: url, attribute: value as! T))

Note the use of as! here, you must match the attribute name and the type of its value correctly in the function call or you will get a runtime abort. Handling this as a soft error, if needed, is left as an exercise.

There are a number of typos etc. in the code you posted, they are left for you to fix, having done that your function should work. A call might look like:

let ans = sortFileArrayByType2(fileAttributeKeyString: "NSFileCreationDate",
                               attributeType: Date.self,
                               fileURLArray: urlArray)

and the type of ans in this case will be [(url: URL, attribute: Date)]

HTH

Upvotes: 4

Mr.P
Mr.P

Reputation: 1440

So I think I know what you're getting at and this is what I've come up with:

func checkType<T>(_ type: T.Type) {
    if type.self == String.self {
        print("It's a string!")
    } else if type.self == Int.self {
        print("It's an int!")
    } else {
        print("It's something else...")
    }
}

And then you can call this either by passing in a type directly to it, or by getting the type of a variable and passing that in as follows:

checkType(String.self) // prints "It's a string!"
let number: Int = 1
checkType(type(of: number)) // prints "It's an int!"

Hope this helps!

Upvotes: 0

Related Questions