Lenguaje rápido NSClassFromString

83

¿Cómo lograr la reflexión en Swift Language?

¿Cómo puedo crear una instancia de una clase?

[[NSClassFromString(@"Foo") alloc] init];
Selvin
fuente
1
Intente así, si deja ImplementationClass: NSObject.Type = NSClassFromString (className) como? NSObject.Type {ImplementationClass.init ()}
Pushpa Raja

Respuestas:

37

Solución menos hacky aquí: https://stackoverflow.com/a/32265287/308315

Tenga en cuenta que las clases Swift ahora tienen un espacio de nombres, por lo que en lugar de "MyViewController" sería "AppName.MyViewController"


En desuso desde XCode6-beta 6/7

Solución desarrollada con XCode6-beta 3

Gracias a la respuesta de Edwin Vermeer, pude construir algo para instanciar clases Swift en una clase Obj-C haciendo esto:

// 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
}

EDITAR

También puedes hacerlo en obj-c puro:

- (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);
}

¡Espero que esto ayude a alguien!

Kevin Delord
fuente
2
A partir de la versión beta 7, esto ya no funcionará. NSStringFromClass ahora solo devolverá el nombre del paquete más el nombre de la clase separados por un punto. Entonces, podría usar código como: var appName: String = NSBundle.mainBundle (). ObjectForInfoDictionaryKey ("CFBundleName") como String? dejar que classStringName: String = NSStringFromClass (theObject.dynamicType) classStringName.stringByReplacingOccurrencesOfString retorno (+ appName, withString: "", opciones: NSStringCompareOptions.CaseInsensitiveSearch, rango: "" nil)
Edwin Vermeer
@KevinDelord Usé tu técnica en mi código. Pero la variable appName no devuelve el valor correcto cuando la aplicación tiene un espacio en su nombre. Alguna de idea de cómo arreglarlo ? Ex "Nombre de la aplicación" en lugar de "Nombre_aplicación"
Loadex
No se puede encontrar la función countElements. ¿Alguna ayuda en esto?
C0D3
Usando esto para classStringName en su lugar: deje classStringName = "_TtC (appName! .Characters.count) (appName) (className.characters.count) (className)"
C0D3
@Loadex, esto no funciona si el nombre de su ejecutable tiene un espacio. También debe reemplazar los espacios con guiones bajos. Se sugirió en esta publicación de blog (algo confusa): medium.com/@maximbilan/…. El código de la publicación funcionó, pero la publicación real hablaba de usar el nombre de la aplicación y el nombre de destino, que era diferente de lo que hacía su código.
pegadoj
57

Debes poner por @objc(SwiftClassName)encima de tu clase veloz.
Me gusta:

@objc(SubClass)
class SubClass: SuperClass {...}
Klaus
fuente
2
NSClassFromString()la función necesita un nombre especificado por la @objcatribución.
eonil
1
¡Increíble, algo tan pequeño con un impacto tan grande en mi bienestar mental!
Adrian_H
¿Alguien puede mostrar cómo usar NSClassFromString () después de hacer lo @objc sobre el nombre de su clase? ¡Acabo de obtener AnyClass! regresó
Ryan Bobrowski
Esto funciona para mi. Pero tengo una pregunta sobre por qué @objc(SubClass)funciona, pero @objc class SubClassno.
pyanfield
@pyanfield del documento Swift: "El atributo objc acepta opcionalmente un argumento de atributo único, que consiste en un identificador. El identificador especifica el nombre que se expondrá a Objective-C para la entidad a la que se aplica el atributo objc". Entonces, la diferencia es cómo nuestra subclase Swift obtiene el nombre, será visible para el Objetivo C. En la @objc class SubClassforma, se implica que el nombre es el mismo que el nombre de la Subclase. Y en la @objc(SubClass) class SubClassforma se especifica directamente. Supongo que el compilador simplemente no puede resolverlo por sí mismo en la primera forma por alguna razón.
voiger
56

Esta es la forma en que inicié UIViewController por nombre de clase

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

Más información está aquí

En iOS 9

var className = "YourAppName.TestViewController"
let aClass = NSClassFromString(className) as! UIViewController.Type
let viewController = aClass.init()
Rigel Chen
fuente
11
Si el nombre de la aplicación contiene "-", debe reemplazarse por "_"
Zitao Xiong
@RigelChen buena solución, gracias, pero si solo necesitamos escribir (TestViewController) y no queremos inicializarlo cada vez, ¿qué debemos hacer?
ArgaPK
@RyanHeitner buena solución gracias, pero si solo necesitamos escribir (TestViewController) y no queremos inicializarlo cada vez, ¿qué debemos hacer?
ArgaPK
@argap Crea un singleton y accede a él: deja que viewController = aClass.sharedInstance ()
mahboudz
27

ACTUALIZACIÓN: A partir de la versión beta 6, NSStringFromClass devolverá el nombre del paquete más el nombre de la clase separados por un punto. Entonces será algo como MyApp.MyClass

Las clases Swift tendrán un nombre interno construido que se compone de las siguientes partes:

  • Comenzará con _TtC,
  • seguido de un número que es la longitud del nombre de su aplicación,
  • seguido del nombre de su aplicación,
  • seguido de un número que es la longitud del nombre de su clase,
  • seguido del nombre de su clase.

Entonces su nombre de clase será algo así como _TtC5MyApp7MyClass

Puede obtener este nombre como una cadena ejecutando:

var classString = NSStringFromClass(self.dynamicType)

Actualización en Swift 3 esto ha cambiado a:

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

Usando esa cadena, puede crear una instancia de su clase Swift ejecutando:

var anyobjectype : AnyObject.Type = NSClassFromString(classString)
var nsobjectype : NSObject.Type = anyobjectype as NSObject.Type
var rec: AnyObject = nsobjectype()
Edwin Vermeer
fuente
Esto funcionará solo para las clases NSObject, ¿qué pasa con las clases Swift? De forma predeterminada, no tienen ningún método de inicio y parece que un encasillado al protocolo no funciona, ¿alguna idea?
Stephan
Swift 1.2 / XCode 6.3.1 se bloquea en mi caso con esta construcción
Stephan
He creado una clase de reflexión con soporte para NSCoding, Printable, Hashable, Equatable y JSON github.com/evermeer/EVReflection
Edwin Vermeer
El problema mencionado se solucionó en Swift 2.0 ... vea mi respuesta porque tiene que llamar a init .... solo nsobjectype () ya no es correcto.
Stephan
1
let classString = NSStringFromClass (tipo (de: self))
Abhishek Thapliyal
11

Es casi lo mismo

func NSClassFromString(_ aClassName: String!) -> AnyClass!

Consulte este documento:

https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Misiverse/Foundation_Functions/#//apple_ref/c/func/NSClassFromString

gabuh
fuente
5
Esa función solo funciona con NSClassclases, no con clases Swift. NSClassFromString("String")regresa nil, pero NSClassFromString("NSString")no lo hace.
Cezary Wojcik
No estoy frente a la computadora ... pero podrías intentarlo con: var myVar:NSClassFromString("myClassName")
gabuh
2
@CezaryWojcik: ps Stringno es una clase; es una estructura
newacct
2
@newacct D'oh, tienes razón, mi culpa. Pero también NSClassFromStringregresa nilpara todas las clases de Swift.
Cezary Wojcik
Si una clase está anotada @objc o hereda de NSObject, entonces usa el sistema de objetos Objective-C y participa en NSClassFromString, método swizzling, etc. Sin embargo, si no lo hace, entonces la clase es interna a su código Swift y no No use ese mismo sistema de objetos. No se ha descrito en su totalidad, pero el sistema de objetos de Swift parece tener un límite menos tardío que el de Objective-C.
Bill
9

Pude crear una instancia de un objeto de forma dinámica

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

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

No he encontrado una manera de crear AnyClassdesde un String(sin usar Obj-C). Creo que no quieren que hagas eso porque básicamente rompe el sistema de tipos.

Sulthan
fuente
1
Creo que esta respuesta, aunque no se trata de NSClassFromString, es la mejor aquí para brindar una forma centrada en Swift de realizar la inicialización dinámica de objetos. ¡Gracias por compartir!
Matt Long
¡Gracias a @Sulthan y Matt también por destacar por qué esta respuesta debería estar en la cima!
Ilias Karim
7

Para swift2, creé una extensión muy simple para hacer esto más rápidamente 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()
    }
}

En mi caso, hago esto para cargar el ViewController que quiero:

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)
}
Damien Romito
fuente
6

Esto le dará el nombre de la clase que desea instanciar. Luego, puede usar la respuesta de Edwin para crear una instancia de un nuevo objeto de su clase.

A partir de la beta 6, se _stdlib_getTypeNameobtiene el nombre de tipo alterado de una variable. Pegue esto en un patio de recreo vacío:

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 salida es:

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

La entrada del blog de Ewan Swick ayuda a descifrar estas cadenas: http://www.eswick.com/2014/06/inside-swift/

por ejemplo, _TtSirepresenta el Inttipo interno de Swift .

Klaas
fuente
"Entonces puedes usar la respuesta de Edwin para crear una instancia de un nuevo objeto de tu clase". No creo que el ref. respuesta funciona para PureSwiftClass. por ejemplo, esta clase no tiene un método de inicio por defecto.
Stephan
6

En Swift 2.0 (probado en 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)
}
Ir, dejar
fuente
5

xcode 7 beta 5:

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

cadena de clase

let classString = NSStringFromClass(TestViewController.self)

o

let classString = NSStringFromClass(TestViewController.classForCoder())

inicie una clase UIViewController desde la cadena:

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

Estoy usando esta categoría para 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

    }

}

El uso sería:

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

Creo que tengo razón al decir que no puedes, al menos no con la versión beta actual (2). Ojalá esto sea algo que cambiará en futuras versiones.

Puede usar NSClassFromStringpara obtener una variable de tipo, AnyClasspero parece que no hay forma en Swift de instanciarla. Puede usar un puente a Objective C y hacerlo allí o, si funciona en su caso, volver a usar una declaración de cambio .

Stephen Darlington
fuente
Incluso con Swift 1.2 / XCode 6.3.1 parece imposible, ¿o encontró una solución?
Stephan
2

Aparentemente, ya no es posible crear una instancia de un objeto en Swift cuando el nombre de la clase solo se conoce en tiempo de ejecución. Un contenedor Objective-C es posible para subclases de NSObject.

Al menos puede crear una instancia de un objeto de la misma clase que otro objeto dado en tiempo de ejecución sin un contenedor Objective-C (usando xCode Versión 6.2 - 6C107a):

    class Test : NSObject {}
    var test1 = Test()
    var test2 = test1.dynamicType.alloc()
seb
fuente
Esto no crea una instancia a partir del nombre de la clase.
Stephan
No está claro por su respuesta, parece que no funciona en clases Swift puras, ¿correcto?
Stephan
2

En Swift 2.0 (probado en la beta2 de Xcode 7) funciona así:

protocol Init {
  init()
}

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

Seguro que el tipo que viene NSClassFromStringtiene que implementar este protocolo de inicio.

Espero que esté claro, classNamees una cadena que contiene el nombre de tiempo de ejecución de Obj-C de la clase que por defecto NO es solo "Foo", pero esta discusión no es en mi humilde opinión el tema principal de su pregunta.

Necesita este protocolo porque, por defecto, todas las clases de Swift no implementan un initmétodo.

Stephan
fuente
Si tiene clases pur Swift, vea mis otras preguntas: stackoverflow.com/questions/30111659
Stephan
2

Parece que el encantamiento correcto sería ...

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
}

... donde "p" significa "empaquetado" - un tema distinto.

Pero el elenco crítico de AnyClass a T actualmente causa un bloqueo del compilador, por lo que, mientras tanto, uno debe descomponer la inicialización de k en un cierre separado, que compila bien.

rory
fuente
2

Utilizo diferentes objetivos y, en este caso, no se encuentra la clase rápida. Debe reemplazar CFBundleName con CFBundleExecutable. También arreglé las advertencias:

- (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);
}
Calin Drule
fuente
2

¿No es la solución tan simple como esta?

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

// className = "MyApp.MyClass"
Mark A. Donohoe
fuente
3
La pregunta original era querer clase de cadena, no cadena de clase.
Ryan
1

También en Swift 2.0 (¿posiblemente antes?) Puede acceder al tipo directamente con la dynamicTypepropiedad

es decir

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)

Salida

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

Funciona para mi caso de uso

apocolipse
fuente
1

Prueba esto.

let className: String = String(ControllerName.classForCoder())
print(className)
Himanshu
fuente
1

Lo he implementado así,

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

Swift 5 , fácil de usar, gracias a @Ondrej Rafaj's

  • Código fuente:

    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
    
       }
    
    }
    
  • Llame así:

    guard let ctrl = "ViewCtrl".controller else {
        return
    }
    //  ctrl do sth
    
Perla Negra
fuente
0

Un ejemplo de salto de página que se muestra aquí, ¡la esperanza puede ayudarlo!

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)
}
Tim
fuente
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)
    }
}
SeRG1k
fuente
Gracias por este fragmento de código, que puede proporcionar ayuda inmediata y limitada. Una explicación adecuada mejoraría enormemente su valor a largo plazo al mostrar por qué es una buena solución al problema y lo haría más útil para futuros lectores con otras preguntas similares. Por favor, editar su respuesta a añadir un poco de explicación, incluyendo los supuestos realizados.
iBug
-6

He aquí un buen ejemplo:

class EPRocks { 
    @require init() { } 
}

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

var epawesome = EPAwesome.self(); 
print(epawesome.awesome);
EvilPenguin
fuente
1
Esto no está haciendo lo que pide la pregunta.
ThomasW