web-dev-qa-db-fra.com

Existe-t-il un moyen pour Interface Builder de restituer des vues IBDesignable qui ne remplacent pas drawRect:

Je remplace très rarement drawRect dans mes sous-classes UIView, préférant généralement définir layer.contents avec des images avant le rendu et utilisant souvent plusieurs sous-couches ou sous-vues et les manipulant en fonction de paramètres d'entrée. Existe-t-il un moyen pour IB de restituer ces piles de vues plus complexes? 

60
Hari Karam Singh

Merci, @zisoft pour m'avoir informé de prepareForInterfaceBuilder. Voici quelques nuances avec le cycle de rendu d'Interface Builder qui ont été à l'origine de mes problèmes et qui méritent d'être signalées.

  1. Confirmé: vous n'avez pas besoin d'utiliser -drawRect.  

La définition d'images sur les états de contrôle UIButton fonctionne. Les piles de couches arbitraires semblent fonctionner si quelques éléments sont pris en compte ...

  1. IB utilise initWithFrame: 

..pas initWithCoder. awakeFromNib est également appelé PAS.

  1. init... n'est appelé qu'une fois par session

C'est à dire. Une fois par recompilation chaque fois que vous apportez une modification au fichier. Lorsque vous modifiez les propriétés IBInspectable, init n'est PAS appelé à nouveau. Toutefois...

  1. prepareForInterfaceBuilder est appelé à chaque changement de propriété

C'est comme avoir KVO sur tous vos IBInspectables ainsi que sur d'autres propriétés intégrées. Vous pouvez le tester vous-même en appelant votre méthode _setup, d'abord uniquement à partir de votre méthode init... Changer un IBInspectable n'aura aucun effet. Ajoutez ensuite l'appel à prepareForInterfaceBuilder. Whahla! Notez que votre code d'exécution aura probablement besoin de KVO supplémentaire car il n'appellera pas la méthode prepareForIB. Plus sur ce ci-dessous ...

  1. init... est trop tôt pour dessiner, définir le contenu du calque, etc.

Au moins avec ma sous-classe UIButton, appeler [self setImage:img forState:UIControlStateNormal] n’a aucun effet dans IB. Vous devez l’appeler depuis prepareForInterfaceBuilder ou via un hook KVO.

  1. Lorsque IB ne parvient pas à rendre, il n'annule pas votre composant mais conserve plutôt la dernière version réussie.

Cela peut être déroutant lorsque vous apportez des modifications sans effet. Vérifiez les journaux de construction.

  1. Astuce: Gardez Activity Monitor à proximité

Je suis suspendu tout le temps à un couple de processus de support différents et ils détruisent toute la machine. Appliquez Force Quit librement.

(UPDATE: Cela n’est plus vraiment le cas depuis la sortie de la version bêta de XCode6. Il ne se bloque plus)

METTRE À JOUR

  1. 6.3.1 ne semble pas aimer KVO dans la version IB. Vous semblez maintenant avoir besoin d'un indicateur pour intercepter Interface Builder sans configurer les KVO. Ceci est correct car la méthode prepareForInterfaceBuilder permet effectivement à KVO d'utiliser toutes les propriétés IBInspectable. Il est regrettable que ce comportement ne soit pas reflété d'une manière ou d'une autre lors de l'exécution, nécessitant ainsi le KVO manuel. Voir l'exemple de code mis à jour ci-dessous.

Exemple de sous-classe UIButton

Vous trouverez ci-dessous un exemple de code d’une sous-classe IBDesignable UIButton en état de fonctionnement. ~~ Remarque, prepareForInterfaceBuilder n'est pas nécessaire car KVO écoute les modifications apportées à nos propriétés pertinentes et déclenche une mise à jour. ~~ UPDATE: voir le point 8 ci-dessus.

IB_DESIGNABLE
@interface SBR_InstrumentLeftHUDBigButton : UIButton

@property (nonatomic, strong) IBInspectable  NSString *topText;
@property (nonatomic) IBInspectable CGFloat topTextSize;
@property (nonatomic, strong) IBInspectable NSString *bottomText;
@property (nonatomic) IBInspectable CGFloat bottomTextSize;
@property (nonatomic, strong) IBInspectable UIColor *borderColor;
@property (nonatomic, strong) IBInspectable UIColor *textColor;

@end



@implementation HUDBigButton
{
    BOOL _isInterfaceBuilder;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self _setup];

    }
    return self;
}

//---------------------------------------------------------------------

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self _setup];
    }
    return self;
}

//---------------------------------------------------------------------

- (void)_setup
{
    // Defaults.  
    _topTextSize = 11.5;
    _bottomTextSize = 18;
    _borderColor = UIColor.whiteColor;
    _textColor = UIColor.whiteColor;
}

//---------------------------------------------------------------------

- (void)prepareForInterfaceBuilder
{
    [super prepareForInterfaceBuilder];
    _isInterfaceBuilder = YES;
    [self _render];
}

//---------------------------------------------------------------------

- (void)awakeFromNib
{
    [super awakeFromNib];
    if (!_isInterfaceBuilder) { // shouldn't be required but jic...

        // KVO to update the visuals
        @weakify(self);
        [self
         bk_addObserverForKeyPaths:@[@"topText",
                                     @"topTextSize",
                                     @"bottomText",
                                     @"bottomTextSize",
                                     @"borderColor",
                                     @"textColor"]
         task:^(id obj, NSDictionary *keyPath) {
             @strongify(self);
             [self _render];
         }];
    }
}

//---------------------------------------------------------------------

- (void)dealloc
{
    if (!_isInterfaceBuilder) {
        [self bk_removeAllBlockObservers];
    }
}

//---------------------------------------------------------------------

- (void)_render
{
    UIImage *img = [SBR_Drawing imageOfHUDButtonWithFrame:self.bounds
                                                edgeColor:_borderColor
                                          buttonTextColor:_textColor
                                                  topText:_topText
                                              topTextSize:_topTextSize
                                               bottomText:_bottomText
                                       bottomTextSize:_bottomTextSize];

    [self setImage:img forState:UIControlStateNormal];
}

@end
129
Hari Karam Singh

Cette réponse est liée à l’attrait de drawRect, mais elle peut peut-être donner quelques idées:

J'ai une classe UIView personnalisée qui comporte des dessins complexes dans drawRect. Vous devez faire attention aux références qui ne sont pas disponibles au moment de la conception, c’est-à-dire UIApplication. Pour cela, je remplace la variable prepareForInterfaceBuilder par un drapeau booléen que j'utilise dans drawRect pour distinguer le temps d'exécution du temps de conception:

@IBDesignable class myView: UIView {
    // Flag for InterfaceBuilder
    var isInterfaceBuilder: Bool = false    

    override init(frame: CGRect) {
        super.init(frame: frame)
        // Initialization code
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func prepareForInterfaceBuilder() {
        self.isInterfaceBuilder = true
    }

    override func drawRect(rect: CGRect)
    {
        // rounded cornders
        self.layer.cornerRadius = 10
        self.layer.masksToBounds = true

        // your drawing stuff here

        if !self.isInterfaceBuilder {
            // code for runtime
            ...
        }

    }

}

Voici à quoi cela ressemble dans InterfaceBuilder:

enter image description here

15
zisoft

Vous n'avez pas besoin d'utiliser drawRect, mais vous pouvez créer votre interface personnalisée dans un fichier xib, la charger dans initWithCoder et initWithFrame. Le rendu sera actif dans IB après l'ajout d'IBDesignable. Consultez ce court tutoriel: https://www.youtube.com/watch?v=L97MdpaF3Xg

9
Leszek Szary

Je pense que layoutSubviews est le mécanisme le plus simple.

Voici un exemple (beaucoup) plus simple dans Swift:

@IBDesignable
class LiveLayers : UIView {

    var circle:UIBezierPath {
        return UIBezierPath(ovalInRect: self.bounds)
    }

    var newLayer:CAShapeLayer {
        let shape = CAShapeLayer()
        self.layer.addSublayer(shape)
        return shape
    }
    lazy var myLayer:CAShapeLayer = self.newLayer

    // IBInspectable proeprties here...
    @IBInspectable var pathLength:CGFloat = 0.0 { didSet {
        self.setNeedsLayout()
    }}

    override func layoutSubviews() {
        myLayer.frame = self.bounds // etc
        myLayer.path = self.circle.CGPath
        myLayer.strokeEnd = self.pathLength
    }
}

Je n'ai pas testé cet extrait, mais j'ai déjà utilisé des modèles comme celui-ci. Notez l'utilisation de la propriété lazy déléguant à une propriété calculée pour simplifier la configuration initiale.

7
Chris Conover

Dans mon cas, il y avait deux problèmes:

  1. Je n'ai pas implémenté initWithFrame dans la vue personnalisée: (Habituellement, initWithCoder: est appelé lors de l'initialisation via IB, mais pour une raison quelconque, initWithFrame: est nécessaire pour IBDesignable uniquement. N'est pas appelé lors de l'exécution lorsque vous implémentez via IB)

  2. La nib de ma vue personnalisée se chargeait depuis mainBundle: [NSBundle bundleForClass:[self class]] était nécessaire.

4
Jakub Truhlář

Pour élaborer la réponse de Hari Karam Singh , ce diaporama explique plus avant:

http://www.splinter.com.au/presentations/ibdesignable/

Ensuite, si vous ne voyez pas vos modifications apparaître dans Interface Builder, essayez les menus suivants:

  • Xcode-> Editeur-> Actualiser automatiquement les vues
  • Xcode-> Editeur-> Actualiser toutes les vues
  • Xcode-> Editor-> Déboguer les vues sélectionnées

Malheureusement, le débogage de ma vue a figé Xcode, mais cela devrait fonctionner pour de petits projets (YMMV).

3
Zack Morris

Je pense que vous pouvez implémenter prepareForInterfaceBuilder et effectuer votre travail d'animation principal pour l'afficher dans IB . J'ai créé des choses fantaisistes avec les sous-classes d'UIButton qui effectuent leur propre travail de couche d'animation principale pour dessiner des bordures ou des arrière-plans. et ils rendent le rendu dans le constructeur d’interface très bien, alors j’imagine que si vous sous-classez UIView directement, alors prepareForInterfaceBuilder est tout ce que vous devez faire différemment. Gardez toutefois à l'esprit que cette méthode n'est exécutée que par IB.

Édité pour inclure le code comme demandé

J'ai quelque chose de similaire à, mais pas exactement comme ça (désolé, je ne peux pas vous donner ce que je fais vraiment, mais c'est une affaire de travail)

class BorderButton: UIButton {
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    func commonInit(){
        layer.borderWidth = 1
        layer.borderColor = self.tintColor?.CGColor
        layer.cornerRadius = 5    
    }

    override func tintColorDidChange() {
        layer.borderColor = self.tintColor?.CGColor
    }

    override var highlighted: Bool {
        willSet {
            if(newValue){
                layer.backgroundColor = UIColor(white: 100, alpha: 1).CGColor
            } else {
                layer.backgroundColor = UIColor.clearColor().CGColor
            }
        }
    }
}

Je substitue à la fois initWithCoder et initWithFrame parce que je veux pouvoir utiliser le composant dans le code ou dans IB (et comme d'autres réponses l'indiquent, vous devez implémenter initWithFrame pour que IB soit heureux.

Ensuite, dans commonInit, j'ai configuré l'animation de base pour dessiner une bordure et la rendre jolie.

J'implémente également une variable willSet pour la variable en surbrillance afin de changer la couleur d'arrière-plan car je déteste quand les boutons dessinent des bordures, mais je ne donne pas de commentaires lorsque vous appuyez dessus (je déteste quand le bouton enfoncé ressemble au bouton non pressé)

2
Alpine

Swift 3 macro

#if TARGET_INTERFACE_BUILDER
#else
#endif

et classe avec fonction qui est appelée quand IB rend le storyboard

@IBDesignable
class CustomView: UIView
{
    @IBInspectable
    public var isCool: Bool = true {
        didSet {
            #if TARGET_INTERFACE_BUILDER

            #else

            #endif
        }
    }

    override func prepareForInterfaceBuilder() {
        // code
    }
}

IBInspectable peut être utilisé avec les types ci-dessous

Int, CGFloat, Double, String, Bool, CGPoint, CGSize, CGRect, UIColor, UIImage
0
Ako