Search code examples
iosswiftfile-storage

Swift 5: saving complex objects to file storage


I am working on a school assignment for which I have to develop an iOS application in Swift 5. The application needs to utilize a web service (a web-API) and either file storage or user defaults.

I had chosen to develop a "QR code manager", in which users can create QR codes for a URL by setting a few design parameters, which are then sent to a generator API. This API (upon an OK request) returns an image in a specified format (PNG in my case).

I have a class with the URL and all the design properties of the QR code, which will also contain the image itself. Please see below code snippet for the class.

public class QRCode {
    
    var bsId : Int?
    var url : String?
    var name: String?
    var frame: Frame?
    var logo: QrCodeLogo?
    var marker: Marker?
    var color : String?
    var bgColor : String?
    var image : Data?
    
    
    init(data: [String:String]) {
        self.url = data["url"]
        self.frame = Frame.allCases.first(where: { $0.description == data["frame"] })
        self.logo = QrCodeLogo.allCases.first(where: { $0.description == data["logo"] })
        self.marker = Marker.allCases.first(where: { $0.description == data["marker"] })
        self.bgColor = data["backGroundColor"]
        self.color = data["colorLight"]
    }
    
    init(json: String) {
        // todo
    }
}

extension QRCode {
    func toDict() -> [String:Any] {
        var dict = [String:Any]();
        let otherSelf = Mirror(reflecting: self);
        for child in otherSelf.children {
            if let key = child.label {
                dict[key] = child.value;
            }
        }
        return dict;
    }
}

All the properties are nullable for ease of development, the class will be further refactored once I have successfully implemented everything.

I have tried various methods I found all over the internet, one of which can be found in the class extension. The function toDict() translates the object properties and their values to a Dictionary object of type [String:Any]. However, I read that when the Any datatype is encoded and then decoded, Swift cannot determine which complex datatype the decoded data is supposed to be, effectively rendering the data either meaningless or unusable.

Another method I found was through extending the Codable-protocol in the class. As far as I am aware however, Codable only accepts primitive datatypes.

Please find below my currently written code for file storage handling. It is not complete yet, but I felt it was a good start and might help in this question.


class StorageManager {
    
    fileprivate let filemanager: FileManager = FileManager.default;
    
    fileprivate func filePath(forKey key: String) -> URL? {
        guard let docURL = filemanager.urls(for: .documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first else {
            return nil;
        }
        
        return docURL.appendingPathComponent(key);
    }
    
    func writeToStorage(identifier: String, data: QRCode) -> Void {
        guard let path = filePath(forKey: identifier) else {
            throw ApplicationErrors.runtimeError("Something went wrong writing the file to storage");
        }
        
        let dict = data.toDict();
        // TODO:: Implement
    }
    
    func readFromStorage(identifier: String) -> Any {
        // TODO:: Implement
        return 0;
    }
    
    func readAllFromStorage() throws -> [URL] {
        let docsURL = filemanager.urls(for: .documentDirectory, in: .userDomainMask)[0];
        
        do {
            let fileURLs = try filemanager.contentsOfDirectory(at: docsURL, includingPropertiesForKeys: nil);
            
            return fileURLs;
        } catch {
            throw ApplicationErrors.runtimeError("Something went wrong retrieving the files from \(docsURL.path): \(error.localizedDescription)");
        }
    }
}

I am very new to Swift and I am running stuck on file storage. Is there any way I could store instances of this class in file storage in such a way that I could reïnstantiate this class when I retrieve the data?

Thanks in advance! Please do not hesitate to ask any questions if there are any.

Edit

Based on matt's comment, please find below the code snippets of the Marker, Frame, and QrCodeLogo enums. The Frame enum:


public enum Frame: String, CaseIterable {
    case noFrame
    case bottomFrame
    case bottomTooltip
    case topHeader
    static var count: Int { return 4 }
    
    var description: String {
        switch self {
        case .noFrame:
            return "no-frame"
        case .bottomFrame:
            return "bottom-frame"
        case .bottomTooltip:
            return "bottom-tooltip"
        case .topHeader:
            return "top-header"
        }
    }
}

The QrCodeLogo enum:

public enum QrCodeLogo: String, CaseIterable {
    case noLogo
    case scanMe
    case scanMeSquare
    static var count: Int { return 3 }
    
    var description: String {
        switch self {
        case .noLogo:
            return "no-logo"
        case .scanMe:
            return "scan-me"
        case .scanMeSquare:
            return "scan-me-square"
        }
    }
}

The Marker enum:

public enum Marker: String, CaseIterable {
    case version1
    case version2
    case version3
    case version4
    case version5
    case version6
    case version7
    case version8
    case version9
    case version10
    case version11
    case version12
    case version13
    case version15
    case version16
    static var count: Int { return 15 }
    
    var description: String {
        switch self {
        case .version1:
            return "version1"
        case .version2:
            return "version2"
        case .version3:
            return "version3"
        case .version4:
            return "version4"
        case .version5:
            return "version5"
        case .version6:
            return "version6"
        case .version7:
            return "version7"
        case .version8:
            return "version8"
        case .version9:
            return "version9"
        case .version10:
            return "version10"
        case .version11:
            return "version11"
        case .version12:
            return "version12"
        case .version13:
            return "version13"
        case .version15:
            return "version15"
        case .version16:
            return "version16"
        }
    }
}

All the above enums contain the valid design options for the API I use. They serve as an input restriction to prevent "invalid parameter"-errors from occurring.

Hopefully, this clears things up.

Thanks again!


Solution

  • A type can conform to Codable provided all its properties conform to Codable. All of your properties do conform to Codable except for the enums, and they will conform to Codable if you declare that they do. Thus, this simple sketch of your types compiles:

    public enum Frame: String, Codable {
        case noFrame
        case bottomFrame
        case bottomTooltip
        case topHeader
    }
    public enum QrCodeLogo: String, Codable {
        case noLogo
        case scanMe
        case scanMeSquare
    }
    public enum Marker: String, Codable {
        case version1
        case version2
        case version3
        case version4
        case version5
        case version6
        case version7
        case version8
        case version9
        case version10
        case version11
        case version12
        case version13
        case version15
        case version16
    }
    public class QRCode : Codable {
        var bsId : Int?
        var url : String?
        var name: String?
        var frame: Frame?
        var logo: QrCodeLogo?
        var marker: Marker?
        var color : String?
        var bgColor : String?
        var image : Data?
    }
    

    There are many, many other things about your code that could be improved. You don't need CaseIterable or description for anything. Now that your type is Codable, you can use it to retrieve the values from JSON directly, automatically. If the names of your enum cases do not match the corresponding JSON keys, just make a CodingKey nested enum to act as a bridge.

    In other words, being Codable makes your type both populatable directly from the JSON and serializable to disk.