How to create an IBInspectable of type enum
Asked Answered
S

6

90

enum is not an Interface Builder defined runtime attribute. The following does not show in Interface Builder's Attributes Inspector:

enum StatusShape:Int {
    case Rectangle = 0
    case Triangle = 1
    case Circle = 2
}
@IBInspectable var shape:StatusShape = .Rectangle

From the documentation: You can attach the IBInspectable attribute to any property in a class declaration, class extension, or category for any type that’s supported by the Interface Builder defined runtime attributes: boolean, integer or floating point number, string, localized string, rectangle, point, size, color, range, and nil.

Q: How can I see an enum in Interface Builder's Attributes Inspector?

Sabec answered 11/12, 2014 at 21:30 Comment(2)
Where is enum in that list? Why would you think you can use an enum?Dissuade
It would be nice to be able to pick an enum case straight from IB I suppose, or a UIFont, like the native UIKit objects can.Sabec
S
83

Swift 3

@IBInspectable var shape:StatusShape = .Rectangle merely creates a blank entry in Interface Builder:

Not available in IB

Use an adapter, which will acts as a bridge between Swift and Interface Builder.
shapeAdapter is inspectable from IB:

   // IB: use the adapter
   @IBInspectable var shapeAdapter:Int {
        get {
            return self.shape.rawValue
        }
        set( shapeIndex) {
            self.shape = StatusShape(rawValue: shapeIndex) ?? .Rectangle
        }
    }

Available in IB

Unlike the conditional compilation approach (using #if TARGET_INTERFACE_BUILDER), the type of the shape variable does not change with the target, potentially requiring further source code changes to cope with the shape:NSInteger vs. shape:StatusShape variations:

   // Programmatically: use the enum
   var shape:StatusShape = .Rectangle

Complete code

@IBDesignable
class ViewController: UIViewController {

    enum StatusShape:Int {
        case Rectangle
        case Triangle
        case Circle
    }

    // Programmatically: use the enum
    var shape:StatusShape = .Rectangle

    // IB: use the adapter
    @IBInspectable var shapeAdapter:Int {
        get {
            return self.shape.rawValue
        }
        set( shapeIndex) {
            self.shape = StatusShape(rawValue: shapeIndex) ?? .Rectangle
        }
    }
}

► Find this solution on GitHub.

Sabec answered 12/12, 2014 at 1:8 Comment(7)
Why are you keeping shapeAsInt as a stored property rather than a computed property?Collenecollet
Right. You can't make a property writeonly though... but a computed property doesn't create a backing instance variable like your stored property does.Collenecollet
Added a no backing store solution proposed by @nhgrif.Sabec
Excuse my downvote. I thought I had a working solution with TARGET_INTERFACE_BUILDER but I was misled. Sorry, best of luck here on SOIlysa
fast forward to 2017 does this issue still remain the same?Linders
I was searching if it was possible to use enums or an array etc. However all I can find is information from 2015 or before so I was wondering if in 2017 with xcode 8.2.1 it is still the same as it was 2 years or so agoLinders
No, still a relevant solution as of Xcode 8.2.1. See ViewController.swift line 37 on github.com/SwiftArchitect/SO-27432736: shape is @IBInspectable yet not visible in IBSabec
L
44

Instead of setting your inspectable enums with ints, you could also set them with strings. Although not quite as preferable as a dropdown, at least this option offers some level of readability.

Swift-only Option:

// 1. Set up your enum
enum Shape: String {
    case Rectangle = "rectangle" // lowercase to make it case-insensitive
    case Triangle = "triangle"
    case Circle = "circle"
}


// 2. Then set up a stored property, which will be for use in code
var shape = Shape.Rectangle // default shape


// 3. And another stored property which will only be accessible in IB (because the "unavailable" attribute prevents its use in code)
@available(*, unavailable, message: "This property is reserved for Interface Builder. Use 'shape' instead.")
@IBInspectable var shapeName: String? {
    willSet {
        // Ensure user enters a valid shape while making it lowercase.
        // Ignore input if not valid.
        if let newShape = Shape(rawValue: newValue?.lowercased() ?? "") {
            shape = newShape
        }
    }
}

It is possible to also get this to work with objective-c as well, by adding an initializer to the enum. However, the compiler will only show the "unavailable" error for your IB-only properties in swift code.

Swift Option with Obj-C Compatibility:

@objc enum Shape: Int {
    case None
    case Rectangle
    case Triangle
    case Circle

    init(named shapeName: String) {
        switch shapeName.lowercased() {
        case "rectangle": self = .Rectangle
        case "triangle": self = .Triangle
        case "circle": self = .Circle
        default: self = .None
        }
    }
}

var shape = Shape.Rectangle // default shape

@available(*, unavailable, message: "This property is reserved for Interface Builder. Use 'shape' instead.")
@IBInspectable var shapeName: String? {
    willSet {
        if let newShape = Shape(rawValue: newValue?.lowercased() ?? "") {
            shape = newShape
        }
    }
}
Lorant answered 24/8, 2015 at 23:31 Comment(3)
I like the Swift Only version: it allows for plain English in IB.Sabec
Does anyone know how to get the options presented as a dropdown of strings in IB using this method?Tum
I would like to know if possible how to get this done using a dropdown please.Decury
S
19

I can't remember the swift syntax, but this is how I solved it in obj-c

#if TARGET_INTERFACE_BUILDER
@property (nonatomic) IBInspectable NSInteger shape;
#else
@property (nonatomic) StatusShape shape;
#endif
Sight answered 5/3, 2015 at 23:16 Comment(2)
The only place that I have the conditional assembly is that one spot in the declaration. No conditional assembly anywhere else since an enum is an integer value in obj-c. One spot to remove when apple fixes this (assuming that they will.)Sight
This is a witty and awesome solution, and mentioned here nshipster.com/ibinspectable-ibdesignableIlysa
M
7

For 2020 - @SwiftArchitect answer updated for today:

Here's a typical full example with all of today's syntax

enter image description here

import UIKit

@IBDesignable class ClipLabels: UILabel {
    
    enum Side: Int { case left, right }
    
    var side: Side = .left {
        didSet {
            common()
        }
    }
    
    @available(*, unavailable, message: "IB only")
    @IBInspectable var leftRight01: Int {
        get {
            return self.side.rawValue
        }
        set(index) {
            self.side = Side(rawValue: index) ?? .left
        }
    }
    

and just an example of use ...

switch side {
    case .left:
        textColor = .red
    case .right:
        textColor = .green
    }

For this critical Swift/iOS QA,

• the very old answer of @SwiftArchitect is perfectly correct but

• I've just updated it and added the critical "unavailable" thing, which is now possible in Swift.

Muskellunge answered 2/1, 2020 at 17:40 Comment(0)
G
4

This is an old thread but useful. I have adapted my answer to swift 4.0 and Xcode 9.0 - Swift 4 has its own little issues with this problem. I am having an @IBInspectable variable with enum type and Xcode 9.0 is not happy, showing me this "Property cannot be marked @IBInspectable because its type cannot be representing in Objective-c"

@Eporediese answers this problem (for swift3) in part; using a property for the storyboard but a straight enum for the rest of the code. Below is a more complete code set that gives you a property to work with in both cases.

enum StatusShape: Int {
  case Rectangle = 0
  case Triangle = 1
  case Circle = 2
}
var _shape:StatusShape = .Rectangle  // this is the backing variable

#if TARGET_INTERFACE_BUILDER
  @IBInspectable var shape: Int {    // using backing variable as a raw int

    get { return _shape.rawValue }
    set {
      if _shape.rawValue != newValue {
        _shape.rawValue = newValue
      }
    }
}
#else
var shape: StatusShape {  // using backing variable as a typed enum
  get { return _shape }
  set {
    if _shape != newValue {
      _shape = newValue
    }
  }
}
#endif
Gnarled answered 13/10, 2017 at 22:33 Comment(0)
Y
2

Swift 3 solution based on SwiftArchitect

enum StatusShape: Int {
    case rectangle, triangle, circle
}
var statusShape: StatusShape = .rectangle
#if TARGET_INTERFACE_BUILDER
@IBInspectable var statusShapeIB: Int {
    get { 
        return statusShape.rawValue 
    }
    set { 
        guard let statusShape = StatusShape(rawValue: newValue) else { return }
        self.statusShape = statusShape
    }
}   //convenience var, enum not inspectable
#endif
Yoshikoyoshio answered 9/3, 2017 at 9:50 Comment(1)
If you wrap this IB-inspectable property in a #if TARGET_INTERFACE_BUILDER block it will only affect the rendering in Interface Builder, not at run time. This will likely result in unexpected behavior and you'll always get a warning in the console: Failed to set (statusShapeIB) user defined inspected property ...: this class is not key value coding-compliant for the key statusShapeIB.Quadrisect

© 2022 - 2024 — McMap. All rights reserved.