AlexBrown
AlexBrown

Reputation: 21

Swift: How to make Generics code more clean

I'm searching for the way to make my code more clean and efficient. I Have an "BaseInfo" it has got some properties and also it is a parent to the "SomeInfo", and "AnotherInfo" classes, which have own additional properties. I've made a generic function that saves and gets those objects from UserDefaults. And I have ViewController that's saves and loads info using those functions, considering the type of info it should use. I want to know is there any way to make my code cleaner and get rid of those type casting in my ViewContoller. Here is my info classes:

public class  BaseInfo: Codable{
    var name: String?
    
    init(_ dict: [String: Any]){
        
        name = dict["name"] as? String
    }
}

class AnotherInfo: BaseInfo {
    var secondName: String?
    
    override init(_ dict: [String : Any]) {
        super.init(dict)
        secondName = dict["secondName"] as? String
    }
    
    required init(from decoder: Decoder) throws {
        fatalError("init(from:) has not been implemented")
    }
}

class SomeInfo: BaseInfo {
    var middleName: String?
    
    
    override init(_ dict: [String : Any]) {
        super.init(dict)
        middleName = dict["middleName"]
    }
    
    required init(from decoder: Decoder) throws {
        fatalError("init(from:) has not been implemented")
    }
}

Here is my class that manages info

protocol InfoManagerProtocol {
    static func getInfo(with type: InfoType) -> BaseInfo?
    static func saveInfo <T: BaseInfo>(with type: InfoType, info: T)
}

class InfoManager: InfoManagerProtocol {
    
    static func getInfo<T: BaseInfo>(with type: InfoType) -> T? {
        if let data = UserDefaults.standard.object(forKey: type.toString()) as? Data, let info = try? JSONDecoder().decode(type.typeOfInfo() as! T.Type, from: data){
            return info
        }
        return nil
    }
    
    
    static func saveInfo<T>(with type: InfoType, info: T) where T : BaseInfo {
        if let encodedData = try? JSONEncoder().encode(info){
            UserDefaults.standard.setValue(encodedData, forKey: type.toString())
        }
    }
}

InfoType enum:

enum InfoType: Int{
    case base = 1
    case another = 2
    case some = 3
}

extension InfoType{

    func toString() -> String{
      switch self{
       case .base: 
           return "base"
       case .another: 
           return "another"
       case .some:
           return "some"
      }
    } 
    func typeOfInfo() -> BaseInfo.Type{
        switch self{
        case .base:
            return BaseInfo.self
        case .another:
            return AnotherInfo.self
        case .some:
            return SomeInfo.self
        }
    }
}

and some controller

class ViewCV: UIViewController{
    ......
    // some outlets
    var infoType: InfoType? // info 

    // some func that updates UI
    func updateUI(){
       
       let genericInfo = InfoManager.getInfo(info: .infoType)
       self.someNameOutletLabel.text = genericInfo.name
       self.secondNameOutletLabel?.text = (genericInfo as? AnotherInfo).secondName // here I don't like it
       if let someInfo = genericInfo as? SomeInfo{
          self.secondNameOutletLabel?.text = someInfo.thirdName // or like here I also don't like
       }
   }
}

Looking forward for other critics and advices

Upvotes: 1

Views: 118

Answers (1)

Joakim Danielson
Joakim Danielson

Reputation: 52108

You can simplify your code by skipping the InfoType type and instead define what type being used from the given parameter or return value.

So the protocol becomes

protocol InfoManagerProtocol {
    static func getInfo<T: BaseInfo>() -> T?
    static func saveInfo <T: BaseInfo>(info: T)
}

and then the implementation. Note that instead of the toString in the enum I now use the classname itself as the key

class InfoManager: InfoManagerProtocol {
    static func getInfo<T: BaseInfo>() -> T? {
        if let data = UserDefaults.standard.object(forKey: "\(T.self)") as? Data, let info = try? JSONDecoder().decode(T.self, from: data){
            return info
        }
        return nil
    }

    static func saveInfo<T>(info: T) where T : BaseInfo {
        if let encodedData = try? JSONEncoder().encode(info){
            print(encodedData)
            UserDefaults.standard.set(encodedData, forKey: "\(T.self)")
        }
    }
}

Here is an example of how to use it (assuming that init(from:) has been properly implemented)

let dict: [String: String] = ["name": "Joe", "secondName": "Doe", "middleName": "Jr"]
let another = AnotherInfo(dict)
let some = SomeInfo(dict)

//Here the declaration of the argument tells the generic function what T is
InfoManager.saveInfo(info: another)
InfoManager.saveInfo(info: some)

//And here the declaration of the return value tells the generic function what T is
if let stored:AnotherInfo = InfoManager.getInfo() {
    print(stored, type(of: stored))
}
if let stored:SomeInfo = InfoManager.getInfo() {
    print(stored, type(of: stored))
}

Upvotes: 1

Related Questions