Swift język NSClassFromString

83

Jak osiągnąć refleksję w Swift Language?

Jak mogę utworzyć instancję klasy

[[NSClassFromString(@"Foo") alloc] init];
Selvin
źródło
1
Spróbuj w ten sposób, jeśli pozwól ImplementationClass: NSObject.Type = NSClassFromString (className) as? NSObject.Type {ImplementationClass.init ()}
Pushpa Raja

Odpowiedzi:

37

Mniej hakerskie rozwiązanie tutaj: https://stackoverflow.com/a/32265287/308315

Zauważ, że klasy Swift mają teraz przestrzeń nazw, więc zamiast „MyViewController” będzie to „AppName.MyViewController”


Przestarzałe od XCode6-beta 6/7

Rozwiązanie opracowane przy użyciu XCode6-beta 3

Dzięki odpowiedzi Edwina Vermeera udało mi się zbudować coś, co utworzy instancję klas Swift w klasie Obj-C, wykonując następujące czynności:

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

EDYTOWAĆ

Możesz to również zrobić w czystym 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);
}

Mam nadzieję, że to komuś pomoże!

Kevin Delord
źródło
2
Począwszy od wersji beta 7 to już nie będzie działać. NSStringFromClass zwróci teraz tylko nazwę pakietu i nazwę klasy oddzielone kropką. Możesz więc użyć kodu takiego jak: var appName: String = NSBundle.mainBundle (). ObjectForInfoDictionaryKey ("CFBundleName") jako String? pozwolić classStringName String = NSStringFromClass (theObject.dynamicType) classStringName.stringByReplacingOccurrencesOfString zwrotny (appName + withString: "", opcje: NSStringCompareOptions.CaseInsensitiveSearch, zakres "" zero)
Edwin Vermeer
@KevinDelord Użyłem twojej techniki w moim kodzie. Ale zmienna appName nie zwraca prawidłowej wartości, gdy aplikacja ma spację w swojej nazwie. Masz jakiś pomysł, jak to naprawić? Na przykład „Nazwa aplikacji” zamiast „Nazwa_aplikacji”
Loadex
Nie można znaleźć funkcji countElements. Jakaś pomoc w tej sprawie?
C0D3
Zamiast tego użyj tego dla classStringName: let classStringName = "_TtC (appName! .Characters.count) (appName) (className.characters.count) (className)"
C0D3
@Loadex, to nie działa, jeśli nazwa pliku wykonywalnego zawiera spację. Musisz również zastąpić spacje podkreśleniami. Zasugerowano to w tym (nieco mylącym) poście na blogu: medium.com/@maximbilan/…. Kod w poście działał, ale rzeczywisty post mówił o używaniu nazwy aplikacji i nazwy docelowej, które różniły się od tego, co robi ich kod.
stuckj
57

Musisz postawić @objc(SwiftClassName)ponad swoją szybką klasą.
Lubić:

@objc(SubClass)
class SubClass: SuperClass {...}
klaus
źródło
2
NSClassFromString()funkcja wymaga nazwy określonej przez @objcatrybucję.
eonil
1
Niesamowite, taka mała rzecz, która ma tak ogromny wpływ na moje samopoczucie psychiczne!
Adrian_H
1
czy ktoś może pokazać, jak używać NSClassFromString () po wykonaniu czynności @objc nad nazwą klasy. Właśnie dostałem AnyClass! powrócił
Ryan Bobrowski
Mi to pasuje. Ale mam pytanie, dlaczego @objc(SubClass)działa, ale @objc class SubClassnie?
pyanfield
@pyanfield z dokumentu Swift: „Atrybut objc opcjonalnie akceptuje pojedynczy argument atrybutu, który składa się z identyfikatora. Identyfikator określa nazwę, która ma być ujawniona Objective-C dla jednostki, której dotyczy atrybut objc.” Tak więc różnica polega na tym, w jaki sposób nasza podklasa Swift otrzymuje nazwę, która będzie widoczna dla celu C. W @objc class SubClasspostaci zakłada się, że nazwa jest taka sama jak nazwa podklasy. I w @objc(SubClass) class SubClassformie jest określony bezpośrednio. Wydaje mi się, że kompilator z jakiegoś powodu nie może sam tego rozgryźć w pierwszej formie.
voiger,
56

W ten sposób inicjalizuję wyprowadzenie UIViewController na podstawie nazwy klasy

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

Więcej informacji tutaj

W iOS 9

var className = "YourAppName.TestViewController"
let aClass = NSClassFromString(className) as! UIViewController.Type
let viewController = aClass.init()
Rigel Chen
źródło
11
Jeśli nazwa aplikacji zawiera „-”, należy ją zastąpić
znakiem
@RigelChen fajne rozwiązanie dzięki ale jeśli potrzebujemy tylko typu (TestViewController) i nie chcemy go inicjalizować za każdym razem to co powinniśmy zrobić?
ArgaPK
@RyanHeitner fajne rozwiązanie dzięki, ale jeśli potrzebujemy tylko typu (TestViewController) i nie chcemy go inicjalizować za każdym razem, to co powinniśmy zrobić?
ArgaPK
@argap Utwórz singleton i uzyskaj do niego dostęp: let viewController = aClass.sharedInstance ()
mahboudz
27

AKTUALIZACJA: Począwszy od wersji beta 6 NSStringFromClass zwróci nazwę twojego pakietu oraz nazwę klasy oddzielone kropką. Będzie to więc coś w rodzaju MyApp.MyClass

Klasy Swift będą miały skonstruowaną nazwę wewnętrzną, która składa się z następujących części:

  • Zacznie się od _TtC,
  • po którym następuje liczba będąca długością nazwy aplikacji,
  • po którym następuje nazwa aplikacji,
  • a po nim liczba będąca długością nazwy Twojej klasy,
  • po którym następuje nazwa klasy.

Więc nazwa klasy będzie miała postać _TtC5MyApp7MyClass

Możesz uzyskać tę nazwę jako ciąg, wykonując:

var classString = NSStringFromClass(self.dynamicType)

Aktualizacja w Swift 3 zmieniła się na:

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

Używając tego ciągu, możesz utworzyć instancję swojej klasy Swift, wykonując:

var anyobjectype : AnyObject.Type = NSClassFromString(classString)
var nsobjectype : NSObject.Type = anyobjectype as NSObject.Type
var rec: AnyObject = nsobjectype()
Edwin Vermeer
źródło
To zadziała tylko dla klas NSObject, o co chodzi w klasach Swift. Domyślnie nie mają żadnej metody init, a rzutowanie typu do protokołu wydaje się nie działać, jakiś pomysł?
Stephan
Awaria Swift 1.2 / XCode 6.3.1 w moim przypadku z tą konstrukcją
Stephan
Stworzyłem klasę refleksji z obsługą NSCoding, Printable, Hashable, Equatable i JSON github.com/evermeer/EVReflection
Edwin Vermeer
Wspomniany problem został rozwiązany w Swift 2.0 ... zobacz moją odpowiedź, ponieważ musisz wywołać init ... po prostu nsobjectype () nie jest już poprawne.
Stephan
1
let classString = NSStringFromClass (type (of: self))
Abhishek Thapliyal
11

To prawie to samo

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

Sprawdź ten dokument:

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

gabuh
źródło
5
Ta funkcja działa tylko z NSClassklasami, a nie z klasami Swift. NSClassFromString("String")wraca nil, ale NSClassFromString("NSString")nie.
Cezary Wojcik
Nie jestem przed komputerem ... ale możesz spróbować z: var myVar:NSClassFromString("myClassName")
gabuh
2
@CezaryWojcik: ps Stringnie jest klasą; to jest struct
newacct
2
@newacct D'oh, masz rację, mój błąd. Ale i tak NSClassFromStringwraca nildla wszystkich klas Swift.
Cezary Wojcik
Jeśli klasa jest opatrzona adnotacją @objc lub dziedziczy po NSObject, to używa systemu obiektowego Objective-C i uczestniczy w NSClassFromString, zamianie metod itp. Jeśli jednak tak nie jest, klasa jest wewnętrzna dla kodu Swift i nie Nie używam tego samego systemu obiektów. Nie zostało to w pełni opisane, ale system obiektów Swifta wydaje się być mniej spóźniony niż system Objective-C.
Bill
9

Mogłem dynamicznie utworzyć instancję obiektu

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

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

Nie znalazłem sposobu na tworzenie AnyClassz pliku String(bez użycia Obj-C). Myślę, że nie chcą, żebyś to robił, ponieważ zasadniczo łamie to system typów.

Sulthan
źródło
1
Myślę, że ta odpowiedź, choć nie dotyczy NSClassFromString, jest najlepszą tutaj, jeśli chodzi o zapewnienie skoncentrowanego na Swift sposobu inicjowania dynamicznego obiektu. Dzięki za udostępnienie!
Matt Long
Dziękuję również @Sulthan i Mattowi za podkreślenie, dlaczego ta odpowiedź powinna być najlepsza!
Ilias Karim
7

Dla swift2 stworzyłem bardzo proste rozszerzenie, aby zrobić to szybciej 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()
    }
}

W moim przypadku robię to, aby załadować ViewController, który chcę:

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
źródło
6

W ten sposób otrzymasz nazwę klasy, której chcesz utworzyć instancję. Następnie możesz użyć odpowiedzi Edwinsa, aby utworzyć wystąpienie nowego obiektu swojej klasy.

Od wersji beta 6 _stdlib_getTypeNamepobiera zniekształconą nazwę typu zmiennej. Wklej to do pustego placu zabaw:

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))")

Wynik to:

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

Wpis na blogu Ewana Swicka pomaga rozszyfrować te ciągi: http://www.eswick.com/2014/06/inside-swift/

np. _TtSioznacza wewnętrzny Inttyp Swift .

Klaas
źródło
„Następnie możesz użyć odpowiedzi Edwinsa, aby utworzyć instancję nowego obiektu swojej klasy”. Nie sądzę, żeby ref. odpowiedz praca dla PureSwiftClass. np. domyślnie ta klasa nie ma metody init.
Stephan
6

W Swift 2.0 (testowany w 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)
}
Idź Let
źródło
5

xcode 7 beta 5:

class MyClass {
    required init() { print("Hi!") }
}
if let classObject = NSClassFromString("YOURAPPNAME.MyClass") as? MyClass.Type {
    let object = classObject.init()
}
grudzień
źródło
3

ciąg z klasy

let classString = NSStringFromClass(TestViewController.self)

lub

let classString = NSStringFromClass(TestViewController.classForCoder())

init a UIViewController class from string:

let vcClass = NSClassFromString(classString) as! UIViewController.Type
let viewController = vcClass.init()
Binglin
źródło
3

Używam tej kategorii dla 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

    }

}

Zastosowanie byłoby następujące:

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

Myślę, że mam rację, mówiąc, że nie możesz, przynajmniej nie przy obecnej wersji beta (2). Miejmy nadzieję, że to się zmieni w przyszłych wersjach.

Możesz użyć, NSClassFromStringaby uzyskać zmienną typu, AnyClassale wydaje się, że w języku Swift nie ma możliwości jej utworzenia. Możesz użyć pomostu do celu C i zrobić to tam lub - jeśli zadziała w twoim przypadku - wrócić do używania instrukcji switch .

Stephen Darlington
źródło
Nawet ze Swift 1.2 / XCode 6.3.1 wydaje się to niemożliwe, czy też znalazłeś rozwiązanie?
Stephan
2

Najwyraźniej nie jest już (już) możliwe utworzenie wystąpienia obiektu w Swift, gdy nazwa klasy jest znana tylko w czasie wykonywania. Opakowanie Objective-C jest możliwe dla podklas NSObject.

Przynajmniej możesz utworzyć instancję obiektu tej samej klasy, co inny obiekt podany w czasie wykonywania bez opakowania Objective-C (używając xCode w wersji 6.2 - 6C107a):

    class Test : NSObject {}
    var test1 = Test()
    var test2 = test1.dynamicType.alloc()
seb
źródło
Nie tworzy to instancji na podstawie nazwy klasy.
Stephan
Z Twojej odpowiedzi nie wynika jasno, wygląda na to, że nie działa na czystych klasach Swift, prawda?
Stephan
2

W Swift 2.0 (testowany w wersji beta2 Xcode 7) działa to tak:

protocol Init {
  init()
}

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

Z pewnością typ pochodzący z NSClassFromStringmusi zaimplementować ten protokół inicjujący.

Spodziewam się, że jest jasne, że classNamejest to ciąg znaków zawierający nazwę środowiska wykonawczego Obj-C klasy, która domyślnie NIE jest tylko „Foo”, ale ta dyskusja nie jest głównym tematem twojego pytania.

Potrzebujesz tego protokołu, ponieważ domyślnie wszystkie klasy Swift nie implementują initmetody.

Stephan
źródło
Jeśli masz zajęcia z Pur Swift, zobacz moje inne pytania: stackoverflow.com/questions/30111659
Stephan
2

Wygląda na to, że poprawna inkantacja byłaby ...

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
}

... gdzie „p” oznacza „zapakowany” - odrębny problem.

Ale krytyczne rzutowanie z AnyClass na T obecnie powoduje awarię kompilatora, więc w międzyczasie należy przełączyć inicjalizację k do osobnego zamknięcia, które kompiluje się dobrze.

rory
źródło
2

Używam różnych celów iw tym przypadku nie znaleziono klasy swift. Należy zamienić CFBundleName na CFBundleExecutable. Naprawiłem też ostrzeżenia:

- (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
źródło
2

Czy rozwiązanie nie jest tak proste?

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

// className = "MyApp.MyClass"
Mark A. Donohoe
źródło
3
pierwotnie chodziło o to, aby klasa była z łańcucha, a nie z klasy.
Ryan
1

Również w Swift 2.0 (prawdopodobnie wcześniej?) Możesz uzyskać dostęp do typu bezpośrednio z dynamicTypewłaściwością

to znaczy

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)

Wynik

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

Działa w moim przypadku użycia

apocolipse
źródło
1

Spróbuj tego.

let className: String = String(ControllerName.classForCoder())
print(className)
Himanshu
źródło
1

Wdrożyłem w ten sposób,

if let ImplementationClass: NSObject.Type = NSClassFromString(className) as? NSObject.Type{
   ImplementationClass.init()
}
Pushpa Raja
źródło
1

Swift 5 , łatwy w użyciu dzięki @Ondrej Rafaj's

  • Kod źródłowy:

    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
    
       }
    
    }
    
  • Zadzwoń tak:

    guard let ctrl = "ViewCtrl".controller else {
        return
    }
    //  ctrl do sth
    
czarna Perła
źródło
0

Przykład skoku strony pokazany tutaj, nadzieja może ci pomóc!

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
źródło
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
źródło
Dziękujemy za ten fragment kodu, który może zapewnić ograniczoną, natychmiastową pomoc. Właściwe wyjaśnienie byłoby znacznie poprawić swoją długoterminową wartość pokazując dlaczego jest to dobre rozwiązanie problemu, a byłoby bardziej użyteczne dla czytelników przyszłości z innymi, podobnymi pytaniami. Proszę edytować swoją odpowiedź dodać kilka wyjaśnień, w tym założeń już wykonanych.
iBug
-6

Oto dobry przykład:

class EPRocks { 
    @require init() { } 
}

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

var epawesome = EPAwesome.self(); 
print(epawesome.awesome);
EvilPenguin
źródło
1
To nie robi tego, o co chodzi w pytaniu.
Thomas W.