Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 274
Roi Mulia Avatar asked Jul 14 '19 10:07

Roi Mulia


2 Answers

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
    }
}
like image 74
vadian Avatar answered Nov 11 '22 00:11

vadian


what happens here is kind of self explanatory


JSONDecoder().decode(/* swift here is expecting class or struct that conforms to Codable */.self, from: data)

but let us assume that you can pass a protocol in your protocol

protocol Sticker: Codable {
}

where is the properties that you are expecting swift to decode from data ?

you added the properties in

public struct StickerString: Sticker,  Codable, Equatable { // it should have redundendant conformance as well as you are conforming to Coddle again
    let fontName: String // here is the properties you are expected to be decoded with the coding keys
    let character: String // here is the properties you are expected to be decoded with the coding keys
}

Here is what I suggest you do as long you want the type you want to decode to be dynamic

class GenericService< /* here you can pass your class or struct that conforms to Codable */ GenericResponseModel: Codable> {

func buildObjectFromResponse(data: Data?) -> GenericResponseModel? {
        var object : GenericResponseModel?
        do {
            object = try JSONDecoder().decode(GenericResponseModel.self , from: data!)
        } catch (let error){
            print(error)
        }
        return object
    }

}
  • through this class you can pass any type or even list that conforms to Codable
  • then you will decouple the type checking from the decoding process using the method below
private func handleDecodingTypes (stickers: [Sticker]){
        for sticker in stickers {
            if sticker is StickerString {
                /* do the decoding here */
            }
            if sticker is StickerBitmap {
                /* do the decoding here */
            }
        }
    }
like image 45
karem_gohar Avatar answered Nov 11 '22 02:11

karem_gohar