Swift enums with associated values defaults

In Swift enums are much more powerful than we got used to in other languages. One of the features that makes them more interesting to use is associated values - values that each instance of enum case can carry along with it. We can not have stored properties in the enum, so associated values is basically the only way to store additional data with enum value. Creating an enum value with associated value has a similar semantics as a method call. The difference is - we can not define defaults for parameters which represent associated values.

Here is a real-life example. I was implementing a custom popover presentation for which I have a relative position, view to present popover from and an inset from it along with several other parameters. First I had all of these properties defined as a separate method parameters:

public class PopoverPresentationController {  
    public init(presentedViewController: UIViewController,
                presenting presentingViewController: UIViewController?,
                position: PopoverPosition = .center,
                inset: CGFloat = 8,
                fromView: UIView? = nil,
                passThrough: Bool = false,
                dimBackground: Bool = false,
                onTouchOut: (() -> Void)? = nil) {
    ...
  }
}

First there were just few constructor parameters, but soon their number grow. It was time to refactor. Usual way to solve constructor over-injection is to refactor set of parameters into a new abstraction. It comes to mind pretty fast that position, inset and fromView all describe popover position. I already had a Position enum defined like this:

public enum Position {  
    case bottom
    case top
    case center
}

so I decided to add the rest of the properties to its associated values:

public enum Position {  
    case bottom(fromView: UIView?, inset: CGFloat)
    case top(fromView: UIView?, inset: CGFloat)
    case center(fromView: UIView?)
}

What I liked about that solution is that I could solve the issue that inset actually does not matter for center position, and with enum and associated values I can avoid meaningless parameters.

What I didn't like though was the fact that I loose the ability to use default values. So after this refactoring the code became cluttered with those defaults pretty fast:

showPopoverMessage(message, position: .bottom(fromView: nil, inset: 8))  

So I started to wonder if there is a way to workaround Swift limitation of not being able to specify defaults for associated values. In the end I found few ways to do that.

Enum with static factory methods

The first solution that comes to mind is to define static factory methods with default parameters:

public enum PopoverPosition {  
    case bottom(fromView: UIView?, inset: CGFloat)
    case top(fromView: UIView?, inset: CGFloat)
    case center(fromView: UIView?)

    public static func bottom(fromView: UIView? = nil, inset: CGFloat = 8) -> PopoverPosition {
        return PopoverPosition.bottom(fromView: fromView, inset: inset)
    }
} 

Unfortunately this will not compile - compiler treats static method as redeclaration of enum case (even if different argument labels are used). There are two options here: either to change methods names, i.e. using an external name of first parameter as a method name prefix:

public enum PopoverPosition {  
    case bottom(fromView: UIView?, inset: CGFloat)
    ...
    public static func bottomFrom(_ fromView: UIView? = nil, inset: CGFloat = 8) -> PopoverPosition {
        return PopoverPosition.bottom(fromView: fromView, inset: inset)
    }
} 

or to rename enum cases, i.e. capitalising them:

public enum PopoverPosition {  
    case Bottom(fromView: UIView?, inset: CGFloat)
    ...
    public static func bottom(fromView: UIView? = nil, inset: CGFloat = 8) -> PopoverPosition {
        return PopoverPosition.Bottom(fromView: fromView, inset: inset)
    }
} 

The main downside of this approach is the need to use different names for methods and cases.

Using struct instead of enum

Another pretty obvious option is to switch from enum to struct.

public struct PopoverPosition {

    public enum Position {
        case bottom
        case top
        case center
    }

    public let position: Position
    public let fromView: UIView?
    public let inset: CGFloat

    private init(position: Position, fromView: UIView?, inset: CGFloat) {
        self.position = position
        self.fromView = fromView
        self.inset = inset
    }

    public static func bottom(fromView: UIView? = nil, inset: CGFloat = 8) -> PopoverPosition {
        return PopoverPosition(position: .bottom, fromView: fromView, inset: inset)
    }
}

This is a bit more to type but it solves the issue. With private initialiser we can limit ways to construct the value to only factory methods which makes it closer to enum cases with associated values. The downside is that now we have two types instead of one (struct and enum) and we can not use PopoverPosition value in a switch, we have to use its position property.

Using enum with a builder

Trying to stick with enum I came up with another option - using an inner builder type to scope factory methods and avoid compiler complaining about redeclarations:

public enum Position {  
    case bottom(fromView: UIView?, inset: CGFloat)
    case top(fromView: UIView?, inset: CGFloat)
    case center(fromView: UIView?)

    public enum Builder {
        public static func bottom(fromView: UIView? = nil, inset: CGFloat = 8) -> Position {
            return Position.bottom(fromView: fromView, inset: inset)
        }
        ...
    }

    public static var make: Position.Builder.Type {
        return Position.Builder.self
    }
}

With this now I have a way to construct enum value with default associated values like this:

let position = Position.Builder.bottom()  

Instead of using Builder directly, which looks a bit clumsy we can use a make method defined in enum:

public enum Position {  
    public static var make: Position.Builder.Type {
        return Position.Builder.self
    }
}

let position = Position.make.bottom()  

Conclusion

After all I ended up using a struct instead of enum. Again I end up with preferring something else to enum, which I consider a code smell in general. There is definitely a place for enums in Swift code, and I do use them from time to time, but most of the time they are not the best option.

Update

Thanks to Olivier for pointing me to the proposal that is supposed to fix the issue that caused this post in the first place by normalising associated values representations, which will result in allowing defaults for associated values.

comments powered by Disqus