2014-09-03 5 views
9

Существует небольшая проблема с UINavigationBar, которую я пытаюсь преодолеть. Когда вы скрываете строку состояния с помощью метода prefersStatusBarHidden() в контроллере вида (я не хочу отключать строку состояния для всего приложения), навигационная панель теряет 20pt от ее высоты, которая принадлежит строке состояния. В основном панель навигации сжимается.Метод swizzling in Swift

enter image description here enter image description here

Я пробуя различные обходные пути, но я обнаружил, что каждый из них имел свои недостатки. Затем я наткнулся на этот category, который, используя метод swizzling, добавляет свойство fixedHeightWhenStatusBarHidden в класс UINavigationBar, который решает эту проблему. Я тестировал его в Objective-C, и он работает. Вот header и implementation исходного кода Objective-C.

Теперь, когда я делаю свое приложение в Swift, я попытался перевести его в Swift.

Первой проблемой, с которой я столкнулся, является расширение Swift, которое не может храниться в свойствах. Поэтому мне пришлось рассчитывать на вычисленное свойство, чтобы объявить свойство fixedHeightWhenStatusBarHidden, которое позволяет мне установить значение. Но это еще одна проблема. По-видимому, вы не можете назначать значения для вычисляемых свойств. Вот так.

self.navigationController?.navigationBar.fixedHeightWhenStatusBarHidden = true 

Я получаю ошибку Невозможно присвоить результат этого выражения.

В любом случае ниже мой код. Он компилируется без каких-либо ошибок, но он не работает.

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() метод в середине комментируется, потому что оставить его дает мне ошибку метод переопределение.

Я раньше не делал метод swizzling в Swift или Objective-C, поэтому я не уверен в следующем шаге, чтобы решить эту проблему или даже вообще это возможно.

Может кто-то пролить свет на это?

спасибо.


UPDATE 1: Благодаря newacct, была решена моя первая проблема о свойствах. Но код не работает. Я обнаружил, что выполнение не достигает метода load() в расширении. Из комментариев в this ответ, я узнал, что либо ваш класс должен быть потомком NSObject, что в моем случае, UINavigationBar не имеет прямого потока от NSObject, но он реализует NSObjectProtocol. Поэтому я не уверен, почему это все еще не работает. Другой вариант - добавление @objc, но Swift не позволяет добавлять его в расширения. Ниже приведен обновленный код.

Вопрос по-прежнему открыт.

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: Джаспер помог мне достичь желаемой функциональности, но, видимо, речь идет с парой главных недостатков.

Поскольку метод load() в расширении не стрелял, как предложил Джаспер, я переместил следующий код в метод делегатов didFinishLaunchingWithOptions.

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

И я должен был жёстко возвращаемого значения «истина» в поглотителе из fixedHeightWhenStatusBarHidden собственности, потому что теперь, когда код swizzling выполняет в приложении делегата, вы не можете установить значение из контроллера представления. Это делает избыточность избыточной, когда дело доходит до повторного использования.

Таким образом, вопрос по-прежнему открыт более или менее. Если у кого-то есть идея улучшить его, ответьте.

+0

Почему вы swizzling 'UINavigationBar'? Вы можете сделать это легко с помощью пользовательского подкласса 'UINavigationBar'. Документы для ['UINavigationController'] (https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/) содержат информацию об использовании пользовательского подкласса' UINavigationBar', как и [@ AlexPretzlav's answer] (http://stackoverflow.com/questions/25651081/method-swizzling-in-swift#26641469). –

ответ

15

Objective-C, который использует динамическую отправку поддерживает следующие:

класса метод swizzling :

Вид, который вы используете выше. Все экземпляры класса заменят свой метод на новую реализацию. Новая реализация может необязательно обернуть старый.

Isa-указатель swizzling:

Экземпляр класса устанавливается в новое время выполнения сгенерированного суб-класса. Методы этого экземпляра будут заменены новым методом, который может необязательно обернуть существующий метод.

Пересылка сообщений:

класс действует как прокси к другому классу, выполняя какую-то работу, перед отправкой сообщения на другой обработчик.

Это все варианты мощного шаблона перехвата, на который полагаются многие из лучших функций Cocoa.

Enter Swift:

Swift продолжает традиции АРК, в том, что компилятор будет делать мощные оптимизаций от вашего имени. Он попытается встроить ваши методы или использовать статическую диспетчеризацию или отправку vtable. В то же время все это предотвращает перехват методов (в отсутствие виртуальной машины).Однако вы можете указать Swift, что вы хотите динамическое связывание (и, следовательно, перехват метода), соблюдая следующие правила:

  • Расширение NSObject или использование директивы @objc.
  • Путем добавления динамического атрибута к функции, например public dynamic func foobar() -> AnyObject

В примере указанный выше, эти требования удовлетворяются. Ваш класс является производным от транзитивно NSObject через UIView и UIResponder, однако есть что-то странное этой категории:

  • категория является переопределение метода загрузки, который обычно вызывается один раз для класса. Вероятно, делать это из категории неразумно, и я предполагаю, что, хотя она, возможно, работала до этого, в случае с Swift это не так.

Попробуйте вместо того, чтобы переместить код Swizzling в ваш AppDelegate-х сделал закончить запуск:

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

Привет, спасибо за подробный ответ, Джаспер. Я переместил swizzling код в AppDelegate. Я столкнулся с проблемой неспособности использовать 'self', так как я в AppDelegate. С помощью [this] (http://stackoverflow.com/a/24048694/1077789) ответ я изменил 'self' на' object_getClass (UINavigationBar) 'и запустил приложение, но, к сожалению, это не помогло. – Isuru

+0

Можете ли вы попробовать UINavigationBar.classForCoder()? Если это не работает, кажется, что я ошибаюсь, и я удалю ответ. , , –

+0

'UINavigationBar.classForCoder()' вызывает сбой с ошибкой ** неожиданно найденный nil при распаковке необязательного значения ** в getter 'fixedHeightWhenStatusBarHidden'. – Isuru

2

Первой проблемой, с которой я столкнулся, является расширение Swift, которое не может сохранить .

Прежде всего, категории в Objective-C также не могут иметь сохраненные свойства (называемые синтезированными свойствами). Код Objective-C, с которым вы связаны, использует вычисленное свойство (он предоставляет явные getters и seters).

В любом случае, возвращаясь к вашей проблеме, вы не можете назначить свою собственность, потому что вы определили ее как свойство только для чтения. Для определения чтения-записи вычисленного свойство, вам нужно будет предоставить геттер и сеттер, как это:

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

Спасибо. Однако мой код не работает. Кажется, что метод 'load()' никогда не запускается. Я обнаружил, что класс, который вы подклассифицируете (в моем случае расширение), должен быть получен из 'NSObject' из [this] (http://stackoverflow.com/a/24898473/1077789). Хотя 'UINavigationBar' напрямую не получен из' NSObject', он реализует 'NSObjectProtocol'. Поэтому я смущен, почему это не работает. – Isuru

+0

Также я прокомментировал предыдущий ответ, связанный с ним, который работает для любого класса '@ objc'. Я попытался определить '@ objc' в расширении, но он не разрешен. – Isuru

+0

Мне удалось заставить его работать _but_ с несколькими недостатками. Я обновил вопрос с прогрессом. – Isuru

2

Хотя это не является прямым ответом на ваш Swift вопрос (кажется, что это может быть ошибка, load не работает для Swift расширений NSObject с), однако, я сделать есть решение исходной задачи

я понял, что подклассы UINavigationBa г и предоставление аналогичной реализации на swizzled решения работает в прошивке 7 и 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) 
     } 
    } 
} 

Затем обновите класс навигационной панели в раскадровке или использовать init(navigationBarClass: AnyClass!, toolbarClass: AnyClass!) при построении навигационного контроллера использовать новый класс.

1

Swift не реализует метод класса load() для расширений. В отличие от ObjC, где время выполнения вызывает + нагрузку для каждого класса и категории, в Swift среда выполнения вызывает только метод класса load(), определенный для класса; Swift 1.1 игнорирует методы класса load(), определенные в расширениях. В ObjC для каждой категории допустимо переопределить + нагрузку, чтобы каждый класс/категория мог регистрироваться для уведомлений или выполнять некоторую другую инициализацию (например, swizzling), когда эта категория/класс загружается.

Как и ObjC, вы не должны переопределять метод класса initialize() в своих расширениях для выполнения swizzling. Если бы вы использовали метод класса initialize() в нескольких расширениях, вызывается только одна реализация. Это отличается от метода + load, когда среда выполнения вызывает все реализации при запуске приложения.

Итак, чтобы инициализировать состояние расширения, вы можете вызвать метод из делегата приложения, когда приложение завершит запуск, или из заглушки ObjC, когда среда выполнения вызывает методы + load. Поскольку каждое расширение может иметь метод загрузки, они должны быть однозначно названы. Если предположить, что у вас есть SomeClass, который спускался от NSObject, код для заглушки ObjC будет выглядеть примерно так:

В файле SomeClass + Foo.swift:

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

В вашем SomeClass + Foo.не м файла:

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

Обновление для Swift 4.

Initialize() больше не подвергается: Method 'initialize()' defines Objective-C class method 'initialize', which is not permitted by Swift

Таким образом, способ сделать это сейчас, чтобы запустить свой код Swizzle через общественности статический метод или один синглтон.

Method swizzling in swift 4