2014-09-03 7 views
9

Es gibt ein kleines Problem mit UINavigationBar, die ich versuche zu überwinden. Wenn Sie die Statusleiste mit der Methode prefersStatusBarHidden() in einem View-Controller ausblenden (ich möchte die Statusleiste für die gesamte App nicht deaktivieren), verliert die Navigationsleiste 20pt ihrer Höhe, die zur Statusleiste gehört. Grundsätzlich schrumpft die Navigationsleiste.Methode Swizzling in Swift

enter image description here enter image description here

ich verschiedene Abhilfen versuchte, aber ich fand, dass jeder von ihnen Nachteile hatte. Dann stieß ich auf diese category, die mit der Methode Swizzling, fügt eine Eigenschaft namens fixedHeightWhenStatusBarHidden in die UINavigationBar Klasse, die dieses Problem löst. Ich habe es in Objective-C getestet und es funktioniert. Hier sind die header und die implementation des ursprünglichen Objective-C-Codes.

Jetzt, da ich meine App in Swift mache, habe ich versucht, es in Swift zu übersetzen.

Das erste Problem, mit dem ich konfrontiert wurde, war Swift Extension kann keine gespeicherten Eigenschaften haben. Also musste ich mich mit der berechneten Eigenschaft begnügen, um die fixedHeightWhenStatusBarHidden Eigenschaft zu deklarieren, die es mir ermöglicht, den Wert festzulegen. Aber das löst ein anderes Problem aus. Anscheinend können Sie berechneten Eigenschaften keine Werte zuweisen. Wie so.

self.navigationController?.navigationBar.fixedHeightWhenStatusBarHidden = true 

Ich erhalte die Fehler Kann nicht auf das Ergebnis dieses Ausdrucks zuweisen.

Wie auch immer, unten ist mein Code. Es kompiliert ohne Fehler, aber es funktioniert nicht.

import Foundation 
import UIKit 

extension UINavigationBar { 

    var fixedHeightWhenStatusBarHidden: Bool { 
     return objc_getAssociatedObject(self, "FixedNavigationBarSize").boolValue 
    } 

    func sizeThatFits_FixedHeightWhenStatusBarHidden(size: CGSize) -> CGSize { 

     if UIApplication.sharedApplication().statusBarHidden && fixedHeightWhenStatusBarHidden { 
      let newSize = CGSizeMake(self.frame.size.width, 64) 
      return newSize 
     } else { 
      return sizeThatFits_FixedHeightWhenStatusBarHidden(size) 
     } 

    } 
    /* 
    func fixedHeightWhenStatusBarHidden() -> Bool { 
     return objc_getAssociatedObject(self, "FixedNavigationBarSize").boolValue 
    } 
    */ 

    func setFixedHeightWhenStatusBarHidden(fixedHeightWhenStatusBarHidden: Bool) { 
     objc_setAssociatedObject(self, "FixedNavigationBarSize", NSNumber(bool: fixedHeightWhenStatusBarHidden), UInt(OBJC_ASSOCIATION_RETAIN)) 
    } 

    override public class func load() { 
     method_exchangeImplementations(class_getInstanceMethod(self, "sizeThatFits:"), class_getInstanceMethod(self, "sizeThatFits_FixedHeightWhenStatusBarHidden:")) 
    } 

} 

fixedHeightWhenStatusBarHidden() Methode in der Mitte wird kommentiert, weil es mich verlassen Fehler ein Verfahren Neudeklaration gibt.

Ich habe nicht Methode Swizzling in Swift oder Objective-C vor, so dass ich nicht sicher bin, der nächste Schritt, um dieses Problem zu lösen oder sogar überhaupt möglich ist.

Kann jemand bitte etwas Licht darauf werfen?

Vielen Dank.


UPDATE 1: Dank newacct, meine erste Frage über Eigenschaften gelöst wurde. Aber der Code funktioniert immer noch nicht. Ich habe festgestellt, dass die Ausführung die load() Methode in der Erweiterung nicht erreicht. Von Kommentaren in this Antwort, habe ich gelernt, dass entweder Ihre Klasse von NSObject, die in meinem Fall UINavigationBar nicht direkt von NSObject abstammt abgeleitet werden muss, aber es implementiert NSObjectProtocol. Ich bin also nicht sicher, warum das immer noch nicht funktioniert. Die andere Option ist das Hinzufügen von @objc, aber Swift erlaubt es nicht, es zu Erweiterungen hinzuzufügen. Unten ist der aktualisierte Code.

Das Problem ist noch offen.

import Foundation 
import UIKit 

let FixedNavigationBarSize = "FixedNavigationBarSize"; 

extension UINavigationBar { 

    var fixedHeightWhenStatusBarHidden: Bool { 
     get { 
      return objc_getAssociatedObject(self, FixedNavigationBarSize).boolValue 
     } 
     set(newValue) { 
      objc_setAssociatedObject(self, FixedNavigationBarSize, NSNumber(bool: newValue), UInt(OBJC_ASSOCIATION_RETAIN)) 
     } 
    } 

    func sizeThatFits_FixedHeightWhenStatusBarHidden(size: CGSize) -> CGSize { 

     if UIApplication.sharedApplication().statusBarHidden && fixedHeightWhenStatusBarHidden { 
      let newSize = CGSizeMake(self.frame.size.width, 64) 
      return newSize 
     } else { 
      return sizeThatFits_FixedHeightWhenStatusBarHidden(size) 
     } 

    } 

    override public class func load() { 
     method_exchangeImplementations(class_getInstanceMethod(self, "sizeThatFits:"), class_getInstanceMethod(self, "sizeThatFits_FixedHeightWhenStatusBarHidden:")) 
    } 

} 

UPDATE 2: Jasper hat mir geholfen, um die gewünschte Funktionalität zu erreichen, aber anscheinend kommt es mit ein paar große Nachteile.

Da die Methode load() in der Erweiterung nicht ausgelöst wurde, wie Jasper vorgeschlagen hat, habe ich den folgenden Codeblock in die Methode didFinishLaunchingWithOptions der App-Delegierten verschoben.

method_exchangeImplementations(class_getInstanceMethod(UINavigationBar.classForCoder(), "sizeThatFits:"), class_getInstanceMethod(UINavigationBar.classForCoder(), "sizeThatFits_FixedHeightWhenStatusBarHidden:")) 

Und ich hatte den Rückgabewert auf ‚true‘ in dem Getter von fixedHeightWhenStatusBarHidden Eigenschaft codieren, weil jetzt, dass der Swizzling Code in der AppDelegate ausführt, können Sie nicht einen Wert aus einem View-Controller eingestellt. Dies macht die Erweiterung überflüssig, wenn es um die Wiederverwendbarkeit geht.

So ist die Frage immer noch mehr oder weniger offen. Wenn jemand eine Idee hat, es zu verbessern, bitte antworte.

+0

Warum Swizzling Sie 'UINavigationBar'? Sie können dies leicht mit einer benutzerdefinierten Unterklasse von 'UINavigationBar' tun. Die Dokumentation für ['UINavigationController'] (https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/) enthält Informationen zur Verwendung einer benutzerdefinierten Unterklasse 'UINavigationBar', ebenso wie [AlexPretzlavs Antwort] (http://stackoverflow.com/questions/25651081/method-swizzling-in-swift#26641469). –

Antwort

15

Objective-C, das dynamische Dispatch verwendet unterstützt die folgenden:

Klasse Methode Swizzling :

Die Art, die Sie oben verwenden. Bei allen Instanzen einer Klasse wird ihre Methode durch die neue Implementierung ersetzt. Die neue Implementierung kann optional das Alte umbrechen.

Isa-pointer Swizzling:

Eine Instanz einer Klasse in eine neue Laufzeit generierte Teilklasse festgelegt ist. Die Methoden dieser Instanz werden durch eine neue Methode ersetzt, die optional die vorhandene Methode umbrechen kann.

Nachrichtenweiterleitung:

Eine Klasse fungiert als Proxy in einer anderen Klasse, durch einige Arbeiten durchführen, bevor die Nachricht an einem anderen Handler weitergeleitet werden.

Dies sind alles Varianten des mächtigen Abfangmusters, auf das viele der besten Funktionen von Cocoa angewiesen sind.

Swift eingeben:

Swift setzt die Tradition der ARC, dass die die Compiler leistungsstarken Optimierungen in Ihrem Namen tun. Es wird versuchen, Ihre Methoden zu inline oder verwenden Sie statische Versand oder Vtable-Versand. Diese sind zwar schneller, verhindern jedoch das Abfangen von Methoden (bei Fehlen einer virtuellen Maschine).Sie können jedoch zu Swift anzuzeigen, dass Sie dynamische möchten Bindung (und damit Interception-Methode) durch die Einhaltung der folgenden:

  • von NSObject Erweiterung oder die @objc Direktive.
  • Durch das Hinzufügen der dynamischen Attribut einer Funktion, zB public dynamic func foobar() -> AnyObject

Im Beispiel Sie oben bieten, werden diese Anforderungen erfüllt werden. Ihre Klasse ist abgeleitet transitively von NSObject über UIView und UIResponder, aber es ist etwas Merkwürdiges an dieser Kategorie ist:

  • Die Kategorie wird das Überschreiben der Load-Methode, die in der Regel einmal für eine Klasse aufgerufen werden. Dies aus einer Kategorie zu tun, ist wahrscheinlich nicht weise, und ich vermute, dass es im Falle von Swift nicht funktioniert hat.

Versuchen Sie stattdessen den Swizzling Code in Ihre AppDelegate das tat Finish Start zu bewegen:

//Put this instead in AppDelegate 
method_exchangeImplementations(
    class_getInstanceMethod(UINavigationBar.self, "sizeThatFits:"), 
    class_getInstanceMethod(UINavigationBar.self, "sizeThatFits_FixedHeightWhenStatusBarHidden:")) 
+0

Hallo danke für die detaillierte Antwort, Jasper. Ich habe den Swizzling-Code nach AppDelegate verschoben. Ich bin auf das Problem gestoßen, dass ich "self" nicht verwenden kann, seit ich in AppDelegate bin. Mit Hilfe von [this] (http://stackoverflow.com/a/24048694/1077789) antwort, habe ich 'self' in' object_getClass (UINavigationBar) 'geändert und die App ausgeführt, aber leider keinen Erfolg. – Isuru

+0

Können Sie versuchen, UINavigationBar.classForCoder()? Wenn das nicht funktioniert, dann scheint es, dass ich falsch bin und ich werde die Antwort löschen. . . –

+0

'UINavigationBar.classForCoder()' verursacht einen Absturz mit dem Fehler ** unerwartete gefunden Nil beim Entpacken ein optionaler Wert ** bei Getter von 'FixedHeightWhenStatusBarHidden'. – Isuru

2

Das erste Problem, mit dem ich konfrontiert wurde, war Swift Extension kann Eigenschaften nicht gespeichert haben.

Zunächst einmal können Kategorien in Objective-C auch keine gespeicherten Eigenschaften (sogenannte synthetisierte Eigenschaften) haben. Der Objective-C-Code, mit dem Sie verknüpft sind, verwendet eine berechnete Eigenschaft (sie stellt explizite Getter und Setter bereit).

Wie auch immer, zurück zu Ihrem Problem, können Sie nicht zu Ihrer Eigenschaft zuweisen, weil Sie es als eine schreibgeschützte Eigenschaft definiert. So definieren eine berechnete Eigenschaft read-write, würden Sie benötigen einen Getter und Setter wie diese bieten:

var fixedHeightWhenStatusBarHidden: Bool { 
    get { 
     return objc_getAssociatedObject(self, "FixedNavigationBarSize").boolValue 
    } 
    set(newValue) { 
     objc_setAssociatedObject(self, "FixedNavigationBarSize", NSNumber(bool: newValue), UInt(OBJC_ASSOCIATION_RETAIN)) 
    } 
} 
+0

Danke. Mein Code funktioniert jedoch nicht. Es scheint, dass die 'load()' Methode nie ausgelöst wird. Ich habe festgestellt, dass die Klasse, von der Sie abgeleitet sind (in meinem Fall die Erweiterung), von "NSObject" von [this] abgeleitet sein muss (http://stackoverflow.com/a/24898473/1077789). Obwohl 'UINavigationBar' nicht direkt von' NSObject' abgeleitet ist, implementiert es das 'NSObjectProtocol'. Ich bin verwirrt, warum das nicht funktioniert. – Isuru

+0

Auch ich habe Ihren Kommentar zu der vorherigen Antwort gesehen, die ich über die Arbeit mit der @ @ objc-Klasse geschrieben habe. Ich habe versucht, @ @ objc in der Erweiterung zu definieren, aber es ist nicht erlaubt. – Isuru

+0

Ich schaffte es zu arbeiten _ but_ mit ein paar Nachteile. Ich habe die Frage mit dem Fortschritt aktualisiert. – Isuru

2

Während dies auf Ihre Swift Frage nicht eine direkte Antwort ist (scheint, wie es ein Fehler sein kann, dass load funktioniert nicht für Swift-Erweiterungen von NSObject s), aber ich eine Lösung zu Ihrem ursprünglichen Problem tun

ich, dass UINavigationBa heraus Subklassen r und eine ähnliche Umsetzung in die umgestellte Lösung, die funktioniert in iOS 7 und 8:

class MyNavigationBar: UINavigationBar { 
    override func sizeThatFits(size: CGSize) -> CGSize { 
     if UIApplication.sharedApplication().statusBarHidden { 
      return CGSize(width: frame.size.width, height: 64) 
     } else { 
      return super.sizeThatFits(size) 
     } 
    } 
} 

Dann aktualisieren Sie die Klasse der Navigationsleiste in Ihrem Storyboard oder init(navigationBarClass: AnyClass!, toolbarClass: AnyClass!) verwenden, wenn die Navigationssteuerung des Aufbau die neue Klasse zu verwenden.

1

Swift implementiert die Klassenmethode load() nicht für Erweiterungen. Im Gegensatz zu ObjC, bei dem die Laufzeit für jede Klasse und Kategorie + load aufruft, ruft die Laufzeitumgebung in Swift nur die Klassenmethode load() auf, die für eine Klasse definiert ist. Swift 1.1 ignoriert load() Klassenmethoden, die in Erweiterungen definiert sind. In ObjC ist es akzeptabel, dass jede Kategorie + Lade überschreibt, so dass jede Klasse/Kategorie sich für Benachrichtigungen registrieren oder eine andere Initialisierung (wie Swizzling) durchführen kann, wenn diese Kategorie/Klasse geladen wird.

Wie ObjC sollten Sie die initialize() -Klassenmethode in Ihren Erweiterungen nicht überschreiben, um das Swizzling durchzuführen. Wenn Sie die initialize() - Klassenmethode in mehreren Erweiterungen implementieren würden, würde nur eine Implementierung aufgerufen. Dies unterscheidet sich von der + load-Methode, bei der die Laufzeit beim Start der App alle Implementierungen aufruft.

Um den Status einer Erweiterung zu initialisieren, können Sie entweder die Methode vom Anwendungsdelegaten aufrufen, wenn die Anwendung beendet wird, oder von einem ObjC-Stub, wenn die Laufzeit die + load-Methoden aufruft. Da jede Erweiterung eine Lade-Methode haben kann, sollte sie eindeutig benannt werden. Unter der Annahme, dass Sie Someclass haben, die von NSObject abstammt, würde der Code für den ObjC Stummel etwas wie folgt aussehen:

In Ihrem Someclass + Foo.swift Datei:

extension SomeClass { 
    class func loadFoo { 
     ...do your class initialization here... 
    } 
} 

In Ihrem Someclass + Foo.m-Datei:

@implementation SomeClass(Foo) 
    + (void)load { 
     [self performSelector:@selector(loadFoo); 
    } 
@end 
1

Update für Swift 4.

initialize() nicht mehr ausgesetzt: Method 'initialize()' defines Objective-C class method 'initialize', which is not permitted by Swift

So wie es jetzt zu tun ist, Ihre swizzle Code über ein öffentliches laufen statische Methode oder über ein Singleton.

Method swizzling in swift 4

Verwandte Themen