Search code examples
swiftbundle

Dynamically select a custom class using a string value and instantiate


I plan to use a string value to select a custom defined class. The desired class object is returned using Bundle.main.classNamed.

For better understanding what I want to do, here is a short code example with some custom classes:

class Product: NSObject {
  var name: String

  init(_ name: String) {
    self.name = name
  }
}

class Apple: Product {
  var num: Int

  init(_ num: Int) {
    self.num = num
    super .init("Apple")
  }
}

class Melon: Product {
  var num: Int
  var size: Int

  init(_ num: Int, _ size: Int) {
    self.num = num
    self.size = size
    super .init("Melon")
  }
}

The class Apple and Melon are subclasses of Product, each with its own initialization method. Given a string value like "1000 Apples" I want to instantiate an Apple class. To get the desired class I use Bundle.main.classNamed like this:

let clsName: String = "Apple"

let nameSpace = Bundle.main.infoDictionary!["CFBundleExecutable"] as! String
let loadedClass = Bundle.main.classNamed(nameSpace + "." + "Apple") as! NSObject.Type

How do I instantiate loadedClass?

let apple = loadedClass.init(100)

Gives an Fatal error: Use of unimplemented initializer 'init()' for class 'MyApp.Apple'

let unknownFruit = loadedClass(100)

Gives the Error: Initializing from a metatype value must reference 'init' explicitly

While there are two subclasses with two different initialization methods it should be possible to use loadedClass with its own init method, but this doesn't work so far.


Swift 5.2.4 Xcode 11.5


Solution

  • See my comments under the question to get a context for this answer. But I wonder if you are solving a wrong problem, and it would be better for you to switch to a different design. For example without knowing how your product classes are used, I think it may be a good case for enum:

    enum Product {
    
        case apple(num: Int)
        case melon(num: Int, size: Int)
    
        init?(_ name: String, _ num: Int = 0, _ size: Int = 0) {
            switch name.lowercased() {
            case "apple":
                self = .apple(num: num)
            case "melon":
                self = .melon(num: num, size: size)
            default:
                return nil
            }
        }
    }
    
    extension Product: CustomStringConvertible {
        var description: String {
            switch self {
            case .apple(let num):
                return "apple(\(num))"
            case .melon(let num, let size):
                return "melon(\(num) - \(size)"
            }
        }
    }
    
    let clsName: String = "Apple"
    let apple = Product(clsName, 100)
    print("I am \(apple?.description)") // apple(100)
    

    Anyway, this is more a food for thought, which was too long for comment! Feel free to delete.