Swift: Enum-basierende UIControl Styles (nicht nur für den Interface Builder)

Heutzutage findet ein Kampf um die schönste und die am besten durchgestaltete App in der iOS Welt statt. Die Zeiten sind vorbei als alleinig das moderne Aussehen von iOS 7 Controls ausreichte um Kunden (und deren Kunden) zufrieden zu stellen wenn es um das Aussehen einer neue App ging. Dies ist nichts Neues. Dieses „sattsehen“ an gewohnten Designs konnte man schon zum Ende von iOS 6 sehen als die Standard-Controls im Skeuomorphic Design schon länger als altbacken und „sattgesehen“ galten.

Zum Glück gibt es ja extra Menschen auf dieser Welt was das Handwerk vom Gestalten von UI und UX nicht nur sehr gut beherrschen sondern meist auch noch von der Pike auf gelernt haben. Eventuell deswegen ärgere ich mich in der iOS Welt gefühlt nur noch sehr wenig über hässliche Apps – die in den Charts trenden.

Des einen Freud, des anderen Leid. UI Designer können noch so gute und abgesprochene Vorarbeit im Gestaltungsprozess leisten, am Ende liegt es am ausführenden Entwickler das erdachte App Design auch in baubaren Quelltext umzusetzen.

Hierbei können kleinere Helferlein unnötigen Stress und Aufwand sparen. Vor allem in agilen Prozessen wo es durch aus einmal sein kann, dass eine Überschrift vom Typ A plötzlich eine andere Farbe haben soll oder gar sich ein ganzes Farbkonzept ändert. In solchen Fällen ist es pure Qual eine komplexe App per Hand durchzugehen und im Interface Builder alle Farbwerte, States und Paddings anzupassen.

Aus diesem Grund haben mir meine tollen Kollegen einen Trick beigebracht um sich selbst und damit dem Projekt Zeit zu sparen: Enum-Basierende Styles für (nicht nur) den Interface Builder.

Um dies zu verdeutlichen habe ich eine kleine, simple Demo für einen einfachen Usecase erstellt. Ein UIButton soll je nach Status (Normal, gedrückt, deaktiviert) eine andere Hintergrundfarbe bekommen. Hierzu kommt noch etwas allgemeines Styling.

Enum based Button style

Quelltext

Generell benötigt man für dieses Feature eine eigene, auf beispielsweise UIButton basierende Klasse. Dieser Trick ist auf nahezu alle UIControl-basierenden Klassen anwendbar. Das gesamte Demoprojekt gibt es auf Github.

@IBDesignable
public class Button: UIButton
{
    /// MARK: - Properties -
    
    // 1.
    /// Style identifier
    /// Default value = MyStyle
    @IBInspectable
    public var styleIdentifier: String = "MyStyle"
    {
        didSet
        {
            applyNormalStyle(styleIdentifier)
        }
    }
    
    // 2.
    /// Overrides enabled property
    /// Will apply normal or disabled state
    override public var enabled: Bool
    {
        didSet
        {
            if enabled
            {
                applyNormalStyle(styleIdentifier)
            }
                
                
            else
            {
                applyDisabledStyle(styleIdentifier)
            }
        }
    }
    
    
    // MARK: - Initialization & Setup -
    
    // 3.
    override init(frame: CGRect)
    {
        super.init(frame: frame)
        setup()
    }
    
    
    required public init?(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
    }
    
    
    override public func awakeFromNib()
    {
        super.awakeFromNib()
        setup()
    }
    
    
    // 4.
    override public func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool
    {
        let touched = super.beginTrackingWithTouch(touch, withEvent: event)
        
        if touched
        {
            applyPressedStyle(styleIdentifier)
        }
        
        return touched
    }
    
    
    // 4.
    override public func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?)
    {
        applyNormalStyle(styleIdentifier)
        return super.endTrackingWithTouch(touch, withEvent: event)
    }
    
    
    // 5.
    override public func prepareForInterfaceBuilder()
    {
        setup()
    }
    
    
    // MARK: - Helper
    
    // 6.
    /// Applies default styles
    private func setup()
    {
        // Apply generic styling
        setTitleColor(UIColor.whiteColor(), forState: .Normal)
        setTitleColor(UIColor.whiteColor(), forState: .Selected)
        contentEdgeInsets = UIEdgeInsetsMake(10, 10, 10, 10)

        enabled = (enabled)
        
        // Apply style
        applyNormalStyle(styleIdentifier)
    }
    
    // 7.
    /// Applies normal style
    ///
    /// - parameter styleIdentifier: Defines the requested style
    private func applyNormalStyle(styleIdentifier: String)
    {
        switch styleIdentifier
        {
        case ButtonStyle.MyStyle.rawValue:
            backgroundColor = UIColor.grayColor()
            
        default:
            return
        }
    }
    
    
    // 7.
    /// Applies pressed style
    ///
    /// - parameter styleIdentifier: Defines the requested style
    private func applyPressedStyle(styleIdentifier: String)
    {
        switch styleIdentifier
        {
        case ButtonStyle.MyStyle.rawValue:
            backgroundColor = UIColor.darkGrayColor()

        default:
            return
        }
    }
    
    // 7.
    /// Applies disabled style
    ///
    /// - parameter styleIdentifier: Defines the requested style
    private func applyDisabledStyle(styleIdentifier: String)
    {
        switch styleIdentifier
        {
        case ButtonStyle.MyStyle.rawValue:
            backgroundColor = UIColor.lightGrayColor()
            
        default:
            return
        }
    }
}


// 0.
/// Defines different button styles
public enum ButtonStyle: String
{
    // Default styling (MyStyle)
    case MyStyle = "MyStyle"
    
    // other styles ...
}

Natürlich kann man diese Styles jetzt nicht nur im Interface Builder setzen. Im Quelltext lässt sich der styleIdentifier ebenfalls wie jede andere (öffentliche) Property setzen. Dank des didSet{ … } werden Änderungen auch zur Laufzeit angewendet.

let button             = UIButton(...)
button.styleIdentifier = ButtonStyle.MyStyle.rawValue

Erklärung

0.)
Definiert das Enum mit dem verfügbaren Styles. Der rawValue ist zum einfachen Matching auf String eingestellt.

1.)
Das styleIdentifier-Property enthält dem in Interface Builder eingestellten Wert. Leider unterstützt der IB noch keine Enums als IBInspectable Datenquelle.

2.)
Die überschriebene enabled-Property benötigt man um den aktivierten beziehungsweise reaktivierten Wert Zustand zu erkennen und somit die entsprechenden gestalterischen Werte zu setzen.

3.)
Benötigte Methoden um bei jedem Initialisieren der Control den Style-Prozess anzustoßen.

4.)
Diese Event Handler werden benötigt um den „gedrückt“ Style zu setzen als auch wieder zu entfernen.

5.)
Wichtig um die angewendeten Styles bereits beim ersten Laden im IB sehen zu können.

6.)
Die generische setup()-Methode setzt allgemein gültige Gestaltungsparameter wie in diesem Beispiel die Textfarbe oder die contentEdgeInsets-Property.

7.)
Methoden welche je nach ausgewählten styleIdentifier die entsprechenden Werte setzen.

Lessons learned
@IBDesignables strapazieren Xcode (Version 7.x) wirklich sehr. Die IDE kommt bei komplexen Oberflächen mit vielen solcher Custom Controls an ihre Grenzen und straft den Entwickler mit anderen Kompilierungsvorgängen oder gar mit regelmäßigen abstürzen. Eine Abhilfe ist, die Designables eines Projektes in ein eigenes Framework zu stopfen – deswegen die public-Indikatoren. Dieser Fix hat natürlich auch an sich selbst wieder negative Seiteneffekte, konnte jedoch bei mir für eine größere Linderung bei meinen Xcode Probleme sorgen.

Schreibe einen Kommentar

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s