NSClassFromString en langage Swift


83

Comment réaliser la réflexion en Swift Language?

Comment puis-je instancier une classe

[[NSClassFromString(@"Foo") alloc] init];

1
Essayez comme ceci, si laissez ImplementationClass: NSObject.Type = NSClassFromString (className) as? NSObject.Type {ImplementationClass.init ()}
Pushpa Raja

Réponses:


37

Solution moins piratée ici: https://stackoverflow.com/a/32265287/308315

Notez que les classes Swift sont désormais espacées de noms, donc au lieu de "MyViewController", ce serait "AppName.MyViewController"


Obsolète depuis XCode6-beta 6/7

Solution développée avec XCode6-beta 3

Grâce à la réponse d'Edwin Vermeer, j'ai pu créer quelque chose pour instancier des classes Swift dans une classe Obj-C en faisant ceci:

// swift file
// extend the NSObject class
extension NSObject {
    // create a static method to get a swift class for a string name
    class func swiftClassFromString(className: String) -> AnyClass! {
        // get the project name
        if  var appName: String? = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleName") as String? {
            // generate the full name of your class (take a look into your "YourProject-swift.h" file)
            let classStringName = "_TtC\(appName!.utf16count)\(appName)\(countElements(className))\(className)"
            // return the class!
            return NSClassFromString(classStringName)
        }
        return nil;
    }
}

// obj-c file
#import "YourProject-Swift.h"

- (void)aMethod {
    Class class = NSClassFromString(key);
    if (!class)
        class = [NSObject swiftClassFromString:(key)];
    // do something with the class
}

ÉDITER

Vous pouvez également le faire en pur obj-c:

- (Class)swiftClassFromString:(NSString *)className {
    NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
    NSString *classStringName = [NSString stringWithFormat:@"_TtC%d%@%d%@", appName.length, appName, className.length, className];
    return NSClassFromString(classStringName);
}

J'espère que cela aidera quelqu'un!


2
À partir de la version bêta 7, cela ne fonctionnera plus.NSStringFromClass retournera simplement le nom de votre bundle plus le nom de la classe séparés par un point. Vous pouvez donc utiliser un code comme: var appName: String = NSBundle.mainBundle (). ObjectForInfoDictionaryKey ("CFBundleName") comme chaîne? laissez classStringName: String = NSStringFromClass (theObject.dynamicType) renvoyer classStringName.stringByReplacingOccurrencesOfString (appName + ".", withString: "", options: NSStringCompareOptions.CaseInsensitiveSearch, intervalle: néant)
Edwin

@KevinDelord J'ai utilisé votre technique dans mon code. Mais la variable appName ne renvoie pas la valeur correcte lorsque l'application a un espace dans son nom. Toute idée de comment résoudre ce problème ? Ex "App Name" au lieu de "App_Name"
Loadex

Impossible de trouver la fonction countElements. Une aide à ce sujet?
C0D3

Utiliser plutôt ceci pour classStringName: laissez classStringName = "_TtC (appName! .Characters.count) (appName) (className.characters.count) (className)"
C0D3

@Loadex, cela ne fonctionne pas si le nom de votre exécutable contient un espace. Vous devez également remplacer les espaces par des traits de soulignement. Cela a été suggéré dans ce billet de blog (quelque peu déroutant): medium.com/@maximbilan/…. Le code dans le message fonctionnait, mais le message réel parlait d'utiliser le nom de l'application et le nom de la cible, ce qui était différent de ce que leur code faisait.
stuckj

57

Vous devez mettre @objc(SwiftClassName)au-dessus de votre classe rapide.
Comme:

@objc(SubClass)
class SubClass: SuperClass {...}

2
NSClassFromString()La fonction a besoin d'un nom spécifié par l' @objcattribution.
eonil le

1
Incroyable, une si petite chose avec un impact si énorme sur mon bien-être mental!
Adrian_H

quelqu'un peut-il montrer comment utiliser NSClassFromString () après avoir fait la chose @objc au-dessus de son nom de classe. Je viens de recevoir AnyClass! retourné
Ryan Bobrowski

Ça marche pour moi. Mais j'ai une question sur pourquoi @objc(SubClass)fonctionne, mais @objc class SubClasspas?
pyanfield

@pyanfield de la documentation Swift: "L'attribut objc accepte en option un seul argument d'attribut, qui consiste en un identifiant. L'identifiant spécifie le nom à exposer à Objective-C pour l'entité à laquelle l'attribut objc s'applique." Donc, la différence est de savoir comment notre sous-classe Swift obtient le nom, elle sera visible pour l'Objectif C. Sous la @objc class SubClassforme, le nom est implicite comme étant le même que le nom de la sous-classe. Et sous la @objc(SubClass) class SubClassforme, il est spécifié directement. Je suppose que le compilateur ne peut tout simplement pas le comprendre par lui-même sous la première forme pour une raison quelconque.
voiger

56

C'est ainsi que j'initialise UIViewController par nom de classe

var className = "YourAppName.TestViewController"
let aClass = NSClassFromString(className) as! UIViewController.Type
let viewController = aClass()

Plus d'informations ici

Dans iOS 9

var className = "YourAppName.TestViewController"
let aClass = NSClassFromString(className) as! UIViewController.Type
let viewController = aClass.init()

11
Si le nom de l'application contient "-", il doit être remplacé par "_"
Zitao Xiong

@RigelChen belle solution merci mais si nous avons juste besoin de type (TestViewController) et que nous ne voulons pas l'initialiser à chaque fois, que devons-nous faire?
ArgaPK

@RyanHeitner belle solution merci, mais si nous avons juste besoin de type (TestViewController) et ne voulons pas l'initialiser à chaque fois, que devons-nous faire?
ArgaPK

@argap Créez un singleton et accédez-y: let viewController = aClass.sharedInstance ()
mahboudz

27

MISE À JOUR: À partir de la version bêta 6, NSStringFromClass renverra le nom de votre bundle plus le nom de la classe séparés par un point. Ce sera donc quelque chose comme MyApp.MyClass

Les classes Swift auront un nom interne construit composé des parties suivantes:

  • Il commencera par _TtC,
  • suivi d'un nombre correspondant à la longueur du nom de votre application,
  • suivi du nom de votre application,
  • suivi d'un nombre correspondant à la longueur du nom de votre classe,
  • suivi du nom de votre classe.

Donc, votre nom de classe sera quelque chose comme _TtC5MyApp7MyClass

Vous pouvez obtenir ce nom sous forme de chaîne en exécutant:

var classString = NSStringFromClass(self.dynamicType)

Mise à jour dans Swift 3, ceci est devenu:

var classString = NSStringFromClass(type(of: self))

En utilisant cette chaîne, vous pouvez créer une instance de votre classe Swift en exécutant:

var anyobjectype : AnyObject.Type = NSClassFromString(classString)
var nsobjectype : NSObject.Type = anyobjectype as NSObject.Type
var rec: AnyObject = nsobjectype()

Cela ne fonctionnera que pour les classes NSObject, en ce qui concerne les classes Swift. Par défaut, ils n'ont aucune méthode d'initialisation et un transtypage en protocole ne semble pas fonctionner, aucune idée?
Stephan

Swift 1.2 / XCode 6.3.1crash dans mon cas avec cette construction
Stephan

J'ai créé une classe de réflexion prenant en charge NSCoding, Printable, Hashable, Equatable et JSON github.com/evermeer/EVReflection
Edwin Vermeer

Le problème mentionné est résolu dans Swift 2.0 ... voir ma réponse car vous devez appeler init .... juste nsobjectype () n'est plus correct.
Stephan

1
let classString = NSStringFromClass (type (de: self))
Abhishek Thapliyal

11

5
Cette fonction ne fonctionne qu'avec les NSClassclasses, pas les classes Swift. NSClassFromString("String")revient nil, mais NSClassFromString("NSString")ne le fait pas.
Cezary Wojcik

Je ne suis pas devant l'ordinateur ... mais vous pouvez essayer avec: var myVar:NSClassFromString("myClassName")
gabuh

2
@CezaryWojcik: ps Stringn'est pas une classe; c'est une struct
newacct

2
@newacct D'oh, tu as raison, mon mauvais. Mais NSClassFromStringrevient nilde toute façon pour toutes les classes Swift.
Cezary Wojcik

Si une classe est annotée @objc ou hérite de NSObject, alors elle utilise le système d'objet Objective-C et participe à NSClassFromString, méthode swizzling, etc. Cependant, si ce n'est pas le cas, alors la classe est interne à votre code Swift et ne n'utilisez pas ce même système d'objets. Cela n'a pas été complètement décrit, mais le système d'objet de Swift semble être moins tardif que celui d'Objective-C.
Bill

9

J'ai pu instancier un objet dynamiquement

var clazz: NSObject.Type = TestObject.self
var instance : NSObject = clazz()

if let testObject = instance as? TestObject {
    println("yes!")
}

Je n'ai pas trouvé de moyen de créer à AnyClasspartir d'un String(sans utiliser Obj-C). Je pense qu'ils ne veulent pas que vous fassiez cela parce que cela brise fondamentalement le système de types.


1
Je pense que cette réponse, bien qu'elle ne concerne pas NSClassFromString, est la meilleure ici pour donner une façon centrée sur Swift de faire l'initialisation dynamique des objets. Merci d'avoir partagé!
Matt Long

Merci également à @Sulthan et Matt d'avoir souligné pourquoi cette réponse devrait être au top!
Ilias Karim

7

Pour swift2, j'ai créé une extension très simple pour le faire plus rapidement https://github.com/damienromito/NSObject-FromClassName

extension NSObject {
    class func fromClassName(className : String) -> NSObject {
        let className = NSBundle.mainBundle().infoDictionary!["CFBundleName"] as! String + "." + className
        let aClass = NSClassFromString(className) as! UIViewController.Type
        return aClass.init()
    }
}

Dans mon cas, je fais ceci pour charger le ViewController que je veux:

override func viewDidLoad() {
    super.viewDidLoad()
    let controllers = ["SettingsViewController", "ProfileViewController", "PlayerViewController"]
    self.presentController(controllers.firstObject as! String)

}

func presentController(controllerName : String){
    let nav = UINavigationController(rootViewController: NSObject.fromClassName(controllerName) as! UIViewController )
    nav.navigationBar.translucent = false
    self.navigationController?.presentViewController(nav, animated: true, completion: nil)
}

6

Cela vous donnera le nom de la classe que vous souhaitez instancier. Ensuite, vous pouvez utiliser Edwins answer pour instancier un nouvel objet de votre classe.

À partir de la version bêta 6, _stdlib_getTypeNameobtient le nom de type mutilé d'une variable. Collez ceci dans un terrain de jeu vide:

import Foundation

class PureSwiftClass {
}

var myvar0 = NSString() // Objective-C class
var myvar1 = PureSwiftClass()
var myvar2 = 42
var myvar3 = "Hans"

println( "TypeName0 = \(_stdlib_getTypeName(myvar0))")
println( "TypeName1 = \(_stdlib_getTypeName(myvar1))")
println( "TypeName2 = \(_stdlib_getTypeName(myvar2))")
println( "TypeName3 = \(_stdlib_getTypeName(myvar3))")

La sortie est:

TypeName0 = NSString
TypeName1 = _TtC13__lldb_expr_014PureSwiftClass
TypeName2 = _TtSi
TypeName3 = _TtSS

L'entrée de blog d'Ewan Swick aide à déchiffrer ces chaînes: http://www.eswick.com/2014/06/inside-swift/

par exemple _TtSireprésente le Inttype interne de Swift .


"Ensuite, vous pouvez utiliser Edwins answer pour instancier un nouvel objet de votre classe." Je ne pense pas que le ref. travail de réponse pour PureSwiftClass. par exemple, par défaut, cette classe n'a pas de méthode init.
Stephan

6

Dans Swift 2.0 (testé dans le Xcode 7.01) _20150930

let vcName =  "HomeTableViewController"
let ns = NSBundle.mainBundle().infoDictionary!["CFBundleExecutable"] as! String

// Convert string to class
let anyobjecType: AnyObject.Type = NSClassFromString(ns + "." + vcName)!
if anyobjecType is UIViewController.Type {
// vc is instance
    let vc = (anyobjecType as! UIViewController.Type).init()
    print(vc)
}

5

xcode 7 bêta 5:

class MyClass {
    required init() { print("Hi!") }
}
if let classObject = NSClassFromString("YOURAPPNAME.MyClass") as? MyClass.Type {
    let object = classObject.init()
}

3

chaîne de classe

let classString = NSStringFromClass(TestViewController.self)

ou

let classString = NSStringFromClass(TestViewController.classForCoder())

init une classe UIViewController à partir de la chaîne:

let vcClass = NSClassFromString(classString) as! UIViewController.Type
let viewController = vcClass.init()

3

J'utilise cette catégorie pour Swift 3:

//
//  String+AnyClass.swift
//  Adminer
//
//  Created by Ondrej Rafaj on 14/07/2017.
//  Copyright © 2017 manGoweb UK Ltd. All rights reserved.
//

import Foundation


extension String {

    func convertToClass<T>() -> T.Type? {
        return StringClassConverter<T>.convert(string: self)
    }

}

class StringClassConverter<T> {

    static func convert(string className: String) -> T.Type? {
        guard let nameSpace = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String else {
            return nil
        }
        guard let aClass: T.Type = NSClassFromString("\(nameSpace).\(className)") as? T.Type else {
            return nil
        }
        return aClass

    }

}

L'utilisation serait:

func getViewController(fromString: String) -> UIViewController? {
    guard let viewController: UIViewController.Type = "MyViewController".converToClass() else {
        return nil
    }
    return viewController.init()
}

2

Je pense que j'ai raison de dire que vous ne pouvez pas, du moins pas avec la version bêta actuelle (2). Espérons que c'est quelque chose qui changera dans les versions futures.

Vous pouvez utiliser NSClassFromStringpour obtenir une variable de type AnyClassmais il ne semble y avoir aucun moyen dans Swift de l'instancier. Vous pouvez utiliser un pont vers l'Objectif C et le faire là-bas ou - si cela fonctionne dans votre cas - utiliser une instruction switch .


Même avec Swift 1.2 / XCode 6.3.1, cela ne semble pas possible, ou avez-vous trouvé une solution?
Stephan

2

Apparemment, il n'est plus possible (plus) d'instancier un objet dans Swift lorsque le nom de la classe n'est connu qu'au moment de l'exécution. Un wrapper Objective-C est possible pour les sous-classes de NSObject.

Au moins, vous pouvez instancier un objet de la même classe qu'un autre objet donné lors de l'exécution sans wrapper Objective-C (en utilisant xCode Version 6.2 - 6C107a):

    class Test : NSObject {}
    var test1 = Test()
    var test2 = test1.dynamicType.alloc()

Cela ne crée pas d'instance à partir du nom de la classe.
Stephan

Ce n'est pas clair d'après votre réponse, cela ne semble pas fonctionner sur des classes Swift pures, n'est-ce pas?
Stephan

2

Dans Swift 2.0 (testé dans la version bêta2 de Xcode 7), cela fonctionne comme ceci:

protocol Init {
  init()
}

var type = NSClassFromString(className) as? Init.Type
let obj = type!.init()

Bien sûr, le type provenant de NSClassFromStringdoit implémenter ce protocole d'initialisation.

Je m'attends à ce que ce soit clair, classNameest une chaîne contenant le nom d'exécution Obj-C de la classe qui n'est par défaut PAS juste "Foo", mais cette discussion n'est pas à mon humble avis le sujet principal de votre question.

Vous avez besoin de ce protocole car par défaut, toutes les classes Swift n'implémentent pas de initméthode.


Si vous avez des cours pur Swift, consultez mes autres questions: stackoverflow.com/questions/30111659
Stephan

2

On dirait que l'incantation correcte serait ...

func newForName<T:NSObject>(p:String) -> T? {
   var result:T? = nil

   if let k:AnyClass = NSClassFromString(p) {
      result = (k as! T).dynamicType.init()
   }

   return result
}

... où "p" signifie "emballé" - un problème distinct.

Mais la conversion critique d'AnyClass vers T provoque actuellement un plantage du compilateur, donc en attendant, il faut casser l'initialisation de k dans une fermeture séparée, ce qui se compile correctement.


2

J'utilise des cibles différentes, et dans ce cas, la classe swift n'est pas trouvée. Vous devez remplacer CFBundleName par CFBundleExecutable. J'ai également corrigé les avertissements:

- (Class)swiftClassFromString:(NSString *)className {
    NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"];
    NSString *classStringName = [NSString stringWithFormat:@"_TtC%lu%@%lu%@", (unsigned long)appName.length, appName, (unsigned long)className.length, className];
    return NSClassFromString(classStringName);
}

2

La solution n'est-elle pas aussi simple que cela?

// Given the app/framework/module named 'MyApp'
let className = String(reflecting: MyClass.self)

// className = "MyApp.MyClass"

3
la question originale voulait la classe de la chaîne, pas la chaîne de la classe.
Ryan

1

Aussi dans Swift 2.0 (peut-être avant?) Vous pouvez accéder au type directement avec la dynamicTypepropriété

c'est à dire

class User {
    required init() { // class must have an explicit required init()
    }
    var name: String = ""
}
let aUser = User()
aUser.name = "Tom"
print(aUser)
let bUser = aUser.dynamicType.init()
print(bUser)

Production

aUser: User = {
  name = "Tom"
}
bUser: User = {
  name = ""
}

Fonctionne pour mon cas d'utilisation


1

Essaye ça.

let className: String = String(ControllerName.classForCoder())
print(className)

1

J'ai implémenté comme ça,

if let ImplementationClass: NSObject.Type = NSClassFromString(className) as? NSObject.Type{
   ImplementationClass.init()
}

1

Swift 5 , facile à utiliser, grâce à @Ondrej Rafaj's

  • Code source:

    extension String {
         fileprivate
         func convertToClass<T>() -> T.Type? {
              return StringClassConverter<T>.convert(string: self)
         }
    
    
        var controller: UIViewController?{
              guard let viewController: UIViewController.Type = convertToClass() else {
                return nil
            }
            return viewController.init()
        }
    }
    
    class StringClassConverter<T> {
         fileprivate
         static func convert(string className: String) -> T.Type? {
             guard let nameSpace = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String, let aClass = NSClassFromString("\(nameSpace).\(className)") as? T.Type else {
                 return nil
            }
             return aClass
    
       }
    
    }
    
  • Appelez comme ceci:

    guard let ctrl = "ViewCtrl".controller else {
        return
    }
    //  ctrl do sth
    

0

Un exemple de saut de page montré ici, l'espoir peut vous aider!

let vc:UIViewController = (NSClassFromString("SwiftAutoCellHeight."+type) as! UIViewController.Type).init()
self.navigationController?.pushViewController(vc, animated: true)

// Click the Table response
tableView.deselectRow(at: indexPath, animated: true)
let sectionModel = models[(indexPath as NSIndexPath).section]
var className = sectionModel.rowsTargetControlerNames[(indexPath as NSIndexPath).row]
className = "GTMRefreshDemo.\(className)"
if let cls = NSClassFromString(className) as? UIViewController.Type {
   let dvc = cls.init()
   self.navigationController?.pushViewController(dvc, animated: true)
}

0

Swift3 +

extension String {

    var `class`: AnyClass? {

        guard
            let dict = Bundle.main.infoDictionary,
            var appName = dict["CFBundleName"] as? String
            else { return nil }

        appName.replacingOccurrences(of: " ", with: "_")
        let className = appName + "." + self
        return NSClassFromString(className)
    }
}

Merci pour cet extrait de code, qui pourrait fournir une aide limitée et immédiate. Une explication appropriée améliorerait considérablement sa valeur à long terme en montrant pourquoi c'est une bonne solution au problème, et la rendrait plus utile aux futurs lecteurs avec d'autres questions similaires. Veuillez modifier votre réponse pour ajouter des explications, y compris les hypothèses que vous avez formulées.
iBug

-6

Voici un bon exemple:

class EPRocks { 
    @require init() { } 
}

class EPAwesome : EPRocks { 
    func awesome() -> String { return "Yes"; } 
}

var epawesome = EPAwesome.self(); 
print(epawesome.awesome);

1
Cela ne fait pas ce que demande la question.
ThomasW
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.