Mathieu Rios
Mathieu Rios

Reputation: 386

Initialize a swift variable without knowing the type

I'm building a notification system for my app and I figured out a way to pass multiple types to my model.

I need to define a target, a second target and a third target in this model. These targets are different types according to the notification type. It looks like something like this:

class UserNotification<T1, T2, T3>: Codable {
    let notifyType: String
    let target: T1
    let secondTarget: T2
    let thirdTarget: T3

    enum CodingKeys: String, CodingKey {
        case notifyType = "notify_type"
        case target
        case secondTarget = "second_target"
        case thirdTarget = "third_target"
    }

It's working well as it is. The thing is I need to define a notification variable in on of my views so I can pass notifications from my backend. This is where I'm stuck. How can I define my variable so I can cast types according to my notifyType value ?

class NotificationBox: UITableViewCell, CellElement {
    private var notification: UserNotification<?, ?, ?> // This is where I'm stuck

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }
    
    func setResource(resource: Codable) {
        var notification = resource as! UserNotification<Any, Any, Any>
        switch(notification.notifyType) {
            case "get_badge":
                self.notification = notification as! UserNotification<Any, Badge, Any>
            case "group_reply":
                self.notification = notification as! UserNotification<Message, Debate, Message>
            case "level_unlock":
                self.notification = notification as! UserNotification<User, Any, Any>
            case "get_vote":
                self.notification = notification as! UserNotification<Any, Debate, Any>
            case "group_follow_argument":
                self.notification = notification as! UserNotification<Message, Debate, Message>
            case "user_follow_level_unlock":
                self.notification = notification as! UserNotification<Any, Any, Any>
            case "followed":
                self.notification = notification as! UserNotification<Any, Any, Any>
            case "group_invitation_new":
                self.notification = notification as! UserNotification<Any, Any, Any>
            default:
                self.notification = notification as! UserNotification<Any, Any, Any>
        }
        configure()
    }

Everything is working quite well excepting the variable definition. I don't know how to do it. I tried to define it as:

private var notification: UserNotification<Any, Any, Any>

But it gives me this error:

Cannot assign value of type 'UserNotification<Any, Badge, Any>' to type 'UserNotification<Any, Any, Any>'

Upvotes: 2

Views: 595

Answers (2)

Sweeper
Sweeper

Reputation: 271150

You should use definitely use an enum with associated values here. As Schottky said in their answer, you'll need a custom Codable implementation. Here's an example, implementing just 4 of the cases, assuming that User, Badge, Message, Debate are all Codable.

enum UserNotification: Codable {
    case getBadge(Badge)
    case groupReply(Message, Debate, Message)
    case levelUnlock(User)
    case userFollowLevelUnlock
    
    var notifyType: String {
        switch self {
        case .getBadge:
            return "get_badge"
        case .groupReply:
            return "group_reply"
        case .levelUnlock:
            return "level_unlock"
        case .userFollowLevelUnlock:
            return "user_follow_level_unlock"
        }
    }
    
    enum CodingKeys: String, CodingKey {
        /*
         When decoding/encoding, use convertFromSnakeCase/convertToSnakeCase to convert the key names to/from snake case
         No need to hard code them here
         e.g.
         let decoder = JSONDecoder()
         decoder.keyDecodingStrategy = .convertFromSnakeCase
         */
        case notifyType
        case target
        case secondTarget
        case thirdTarget
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(notifyType, forKey: .notifyType)
        switch self {
        case .getBadge(let badge):
            // you can also encode the unused targets here if your backend needs them
            try container.encode(badge, forKey: .secondTarget)
        case .groupReply(let msg1, let debate, let msg2):
            try container.encode(msg1, forKey: .target)
            try container.encode(debate, forKey: .secondTarget)
            try container.encode(msg2, forKey: .thirdTarget)
        case .levelUnlock(let user):
            try container.encode(user, forKey: .target)
        case .userFollowLevelUnlock:
            break // nothing more to encode in this case
        }
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        switch try container.decode(String.self, forKey: .notifyType) {
        case "get_badge":
            self = .getBadge(
                try container.decode(Badge.self, forKey: .secondTarget)
            )
        case "group_reply":
            self = .groupReply(
                try container.decode(Message.self, forKey: .target),
                try container.decode(Debate.self, forKey: .secondTarget),
                try container.decode(Message.self, forKey: .thirdTarget)
            )
        case "level_unlock":
            self = .levelUnlock(
                try container.decode(User.self, forKey: .target)
            )
        case "user_follow_level_unlock":
            self = .userFollowLevelUnlock
        default:
            throw DecodingError.dataCorruptedError(forKey: .notifyType, in: container, debugDescription: "Unknown notifyType")
        }
    }
}

This way, the notification property can just be:

private var notification: UserNotification

and setResource is trivial.

Upvotes: 2

Schottky
Schottky

Reputation: 2014

Responding to the comments: I think that a better approach could be to use an enum with associated data; something like this:

enum Notification {
    case badge(Badge)
    case groupReply(message: Message, debate: Debate, reply: Message)
    case followed
    // ...
}

If you want to know which notification you have (and also get the associated data), you can do it like so:

switch notification {
    case .badge(let badge):
        // badge is now a let of type Badge
    case .groupReply(let message, _, let reply):
        // you typically ignore unrelated stuff with _
    case .followed:
}

This has the downside that currently you have to provide your own Codable conformance. This feature has been implemented for the next update of swift, however.

Upvotes: 1

Related Questions