Search code examples
iosswiftprotocolscodabledecodable

Protocol type cannot conform to protocol because only concrete types can conform to protocols


Within the app, we have two types of Stickers, String and Bitmap. Each sticker pack could contain both types. This is how I declare the models:

// Mark: - Models

protocol Sticker: Codable {
}

public struct StickerString: Sticker,  Codable, Equatable {
    let fontName: String
    let character: String
}

public struct StickerBitmap: Sticker,  Codable, Equatable {
    let imageName: String
}

After the user chooses some stickers and used them, we want to save the stickers into UserDefaults so we can show him the "Recently Used" Sticker tab. I'm trying to Decode the saved [Sticker] array:

let recentStickers = try? JSONDecoder().decode([Sticker].self, from: data)

But I get the following compile error:

Protocol type 'Sticker' cannot conform to 'Decodable' because only concrete types can conform to protocols

I can't understand why as I declared Sticker as Codable which also implement Decodable. Any help would be highly appreciated!


Solution

  • Rather than protocols use generics.

    Declare a simple function

    func decodeStickers<T : Decodable>(from data : Data) throws -> T
    {
        return try JSONDecoder().decode(T.self, from: data)
    }
    

    T can be a single object as well as an array.


    In your structs drop the Sticker protocol. You can also delete Equatable because it's getting synthesized in structs.

    public struct StickerString : Codable {
        let fontName: String
        let character: String
    }
    
    public struct StickerBitmap : Codable {
        let imageName: String
    }
    

    To decode one of the sticker types annotate the type

    let imageStickers = """
    [{"imageName":"Foo"},{"imageName":"Bar"}]
    """    
    let stickerData = Data(imageStickers.utf8)
    
    let recentStickers : [StickerBitmap] = try! decodeStickers(from: stickerData)
    print(recentStickers.first?.imageName)
    

    and

    let stringSticker = """
    {"fontName":"Times","character":"😃"}
    """    
    let stickerData = Data(stringSticker.utf8)
    
    let sticker : StickerString = try! decodeStickers(from: stickerData)
    print(sticker.character)
    

    To decode an array of StickerString and StickerBitmap types declare a wrapper enum with associated values

    enum Sticker: Codable {
    
        case string(StickerString)
        case image(StickerBitmap)
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            do {
                let stringData = try container.decode(StickerString.self)
                self = .string(stringData)
            } catch DecodingError.keyNotFound {
                let imageData = try container.decode(StickerBitmap.self)
                self = .image(imageData)
            }
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
                case .string(let string) : try container.encode(string)
                case .image(let image) : try container.encode(image)
            }
        }
    }
    

    Then you can decode

    let stickers = """
    [{"imageName":"Foo"},{"imageName":"Bar"}, {"fontName":"Times","character":"😃"}]
    """
    
    let stickerData = Data(stickers.utf8)
    let recentStickers = try! JSONDecoder().decode([Sticker].self, from: stickerData)
    print(recentStickers)
    

    In a table view just switch on the enum

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let sticker = stickers[indexPath.row]
        switch sticker {
        case .string(let stringSticker): 
            let cell = tableView.dequeueReusableCell(withCellIdentifier: "StringStickerCell", for: indexPath) as! StringStickerCell
            // update UI
            return cell
        case .image(let imageSticker): 
            let cell = tableView.dequeueReusableCell(withCellIdentifier: "ImageStickerCell", for: indexPath) as! ImageStickerCell
            // update UI
            return cell
        }
    }