8 Replies
      Latest reply on Sep 12, 2019 9:15 AM by iniitamo
      iniitamo Level 1 Level 1 (10 points)

        Hiya ! I've hit a bit of a hiccup with my code and thought maybe someone might be able to assist. Essentially, I have a collection of classes from which I would like to select one to assign to an optional instance variable. I'm looking to have that selection made by an index value that picks the class name from an array. I'm having trouble figuring out how to determine the class directly from the class name string.

         

        How this is supposed to work: I have a number of classes, in this example Elephant, Gorilla and Tiger. I also have a String array, classNames, with values that match the names of those classes. Additionally, I have a Int variable, classIndex, which is used to select which class to use. What I want is for my instance variable, animal, to be able to assume the selected class after its initial declaration. Finally, I would like animal to be able to run custom methods in the class that it assumed.

         

        Example code:

         

        var animal: Any?
        
        var classNames = ["Elephant", "Gorilla", "Tiger"]
        var classIndex = 2
        
        setupAnimal()
        moveAnimal()
        
        func setupAnimal() {
             if classNames[classIndex] == "Elephant" {
                  animal = Elephant()
                  addChild(animal! as! Elephant)
             } else if classNames[classIndex] == "Gorilla" {
                  animal = Gorilla()
                  addChild(animal! as! Gorilla)
             } else if classNames[classIndex] == "Tiger" {
                  animal = Tiger()
                  addChild(animal! as! Tiger)
             }
        }
        
        func moveAnimal() {
             if classNames[classIndex] == "Elephant" {
                  (animal! as! Elephant).move()
             } else if classNames[classIndex] == "Gorilla" {
                  (animal! as! Gorilla).move()
             } else if classNames[classIndex] == "Tiger" {
                  (animal! as! Tiger).move()
             }
        }
        

         

        By setting animal initially to Any?, I'm able to initiate it later on with any of my classes, in this example Tiger, since classIndex is 2. When I want to access the custom method move() in Tiger, I use the as operator to access animal as a Tiger class. This works all well and good, but it's not dynamic. For me to continuously force the class, I have to hardcode conditionals to check for each class name, which is far from ideal.

         

        Is there a way to avoid these conditionals by determining the class directly from the selected class name: classNames[classIndex]?

        • Re: Assigning a class dynamically from a class name string
          OOPer Level 8 Level 8 (5,305 points)

          First of all, a general advice, avoid using `Any` as far as you can.

           

          You can declare a protocol `Animal`, and make all your animal classes conform to it:

          protocol Animal: class {
              init()
              
              func move()
              
              //...
          }
          
          extension Tiger: Animal {}
          extension Elephant: Animal {}
          extension Gorilla: Animal {}
          
          

          And declare the variable `animal` as `Animal`:

          var animal: Animal?
          
          

           

          And second, you could use `NSClassFromString` in Swift, but in most cases you want to use such feature, there may be a better alternative.

           

          For example, use an Array of classes, rather than an Array of Strings:

          var classes: [Animal.Type] = [Elephant.self, Gorilla.self, Tiger.self]
          var classIndex = 2
          
          func setupAnimal() {
              let cls = classes[classIndex]
              animal = cls.init()
              addChild(animal!)
          }
          
          func moveAnimal() {
              animal?.move()
          }
          
          

           

          You are not showing the whole code, so you may need some modifications, but please try.

            • Re: Assigning a class dynamically from a class name string
              iniitamo Level 1 Level 1 (10 points)

              Thanks for the response, OOPer !

               

              I tried a bunch of stuff, but definitely creating an entity for animal itself helped, and it made things more streamlined in general. So, I proceeded to make a new class Animal, that the animal type classes subclass. I have my reasons for subclassing rather than using a protocol. In fact, this whole animal theme is only here to make things more understandable.

               

              Using a common parent class made things a lot more dynamic and simple. However, I still couldn't get the class-from-string thing to work. I'm trying to avoid hardcoding the collection of Animal subclasses, because the idea is that they come from data and it is assumed that those classes exist. I tried NSClassFromString and Bundle.main.classNamed(), but I just couldn't get them cast and interpreted properly.

               

              My current code:

               

              var animal: Animal?
              
              var classNames = ["Elephant", "Gorilla", "Tiger"]
              var classIndex = 2
              
              setupAnimal()
              moveAnimal()
              
              func setupAnimal() {
                   let animalName = classNames[classIndex]
              
                   if animalName == "Elephant" {
                        animal = Elephant()
                   } else if animalName == "Gorilla" {
                        animal = Gorilla()
                   } else if animalName == "Tiger" {
                        animal = Tiger()
                   }
              
                   addChild(animal?)
              }
              
              func moveAnimal() {
                   animal?.move()
              }
              

               

              What I'm still trying to do is to get rid of the remaining conditionals by determining the class name from the string. So, if animalName == "Tiger" { animal = Tiger() } should become something along the lines of animal = NSClassFromString(classNames[classIndex]), but I don't know what exactly.

                • Re: Assigning a class dynamically from a class name string
                  Claude31 Level 8 Level 8 (6,485 points)

                  A simple way is to have class as @objC (thus need to be NSObject).

                   

                  Here is an example:

                  @objc(Animal)
                  
                  class Animal: NSObject {
                  //    var name: String
                  //
                  //    init(named: String) {
                  //        self.name = named
                  //    }
                  }
                  
                  @objc(Elephant)
                  class Elephant: Animal {
                  }
                  
                  var cls = NSClassFromString("Animal")!
                  print(cls)
                  cls = NSClassFromString("Elephant")!
                  print(cls)

                   

                  you get

                  Animal

                  Elephant

                   

                  And

                  cls = NSClassFromString("Elephant")!.superclass()!
                  print(cls)

                  gets

                  Animal

                  • Re: Assigning a class dynamically from a class name string
                    OOPer Level 8 Level 8 (5,305 points)

                    Interacting with text data looks like the right reason to use `NSClassFromString`.

                     

                    When you want to get a class from its class name in Swift, you need a fully qualified class name, which means, a module name prefixed.

                    x NSClassFromString("Tiger")

                    o NSClassFromString("MyModuleName.Tiger")

                    (Unless you give @objc names as suggested by Claud31.)

                     

                    So, you can write something like this:

                    extension String {
                        func toMyModuleClass() -> AnyClass? {
                            struct My {
                                static let moduleName = String(reflecting: Animal.self).prefix{$0 != "."}
                            }
                            return NSClassFromString("\(My.moduleName).\(self)")
                        }
                    }
                    
                    

                    (You may define `moduleName` with a String literal, if you prefer. Assuming `Animal` and its all subclasses exist in the same module.)

                     

                    And your Animal class needs to have a required initializer to instatiate an instance from its subclasses:

                    class Animal {
                        required init() {
                            //...
                        }
                        
                        func move() {
                            //...
                        }
                        
                        //...
                    }
                    class Elephant: Animal {
                        //...
                    }
                    class Gorilla: Animal {
                        //...
                    }
                    class Tiger: Animal {
                        //...
                    }
                    
                    

                     

                    And then, you can write your `setupAnimal` like this:

                    func setupAnimal() {
                        if let animalClass = classNames[classIndex].toMyModuleClass() as? Animal.Type {
                            animal = animalClass.init()
                            addChild(animal!)
                        } else {
                            print("Class \(classNames[classIndex]) does not exist")
                        }
                    }
                    
                    

                     

                    Or, you can put fully qualified names in your data resource.

                      • Re: Assigning a class dynamically from a class name string
                        Claude31 Level 8 Level 8 (6,485 points)

                        I found this discussion on Swift forum really interesting for this post.

                         

                        h ttps://forums.swift.org/t/pitch-fully-qualified-name-syntax/28482

                        • Re: Assigning a class dynamically from a class name string
                          iniitamo Level 1 Level 1 (10 points)

                          Thanks guys! I tried using Animal.Type to set the class coming from NSClassFromString. The Animal class now also has a required initializer. I didn't really understand module name and fully qualified name stuff. Where should the module name be coming from?

                           

                          Currently the code I have crashes at the NSClassFromString line, assumedly because it can't find the class. Perhaps the missing module name is a reason for that. I'm actually working in SpriteKit, so these classes happen to be subclasses of SKNode: SKNode -> Animal -> Tiger.

                           

                          Current code:

                           

                          var animal: Animal?
                          
                          var classNames = ["Elephant", "Gorilla", "Tiger"]
                          var classIndex = 2
                          
                          setupAnimal()
                          moveAnimal()
                          
                          func setupAnimal() {
                               let animalName = classNames[classIndex]
                          
                               animal = (NSClassFromString(animalName) as! Animal.Type).init()
                          
                               addChild(animal?)
                          }
                          
                          func moveAnimal() {
                               animal?.move()
                          }
                            • Re: Assigning a class dynamically from a class name string
                              OOPer Level 8 Level 8 (5,305 points)

                              It is not so difficult once understood.

                               

                              Assume your project's name is MyProject, and you have some Swift classes:

                              class Animal {
                                  required init() {
                                      //...
                                  }
                                  
                                  func move() {
                                      //...
                                  }
                                  
                                  //...
                              }
                              class Elephant: Animal {
                                  //...
                              }
                              
                              

                               

                              Then you can try:

                                  print(NSClassFromString("Elephant"))
                                  print(NSClassFromString("MyProject.Elephant"))
                              
                                  print(NSStringFromClass(Elephant.self))
                              
                              

                               

                              In this case, "MyProject.Elephant" is the full name of the class Elephant and `NSClassFromString` works only on full names.

                              You need to prefix `MyProject.` to make `NSClassFromString` work.