Swift Enum with Custom Initializer Loses Rawvalue Initializer

Swift enum with custom initializer loses rawValue initializer

This bug is solved in Xcode 7 and Swift 2

Can you initialize an Enum value from the name of its case (*not* its RawValue?)

In Swift 4.2, this is quite easy to do now using CaseIterable.

enum MyOtherEnum: CaseIterable {
case xxx
case yyy
case zzz

init?(caseName: String) {
for value in MyOtherEnum.allCases where "\(value)" == caseName {
self = value
return
}

return nil
}
}

enum MyTestEnum: Int, CaseIterable{
case one = 1
case eight = 8
case unknown = -1

init?(caseName: String) {
for value in MyTestEnum.allCases where "\(value)" == caseName {
self = value
return
}

return nil
}
}

What I am doing here is creating a failable initializer which iterates through all potential cases, testing to see if "\(value)" (which returns the name for that potential case) matches the caseName argument passed in to the initializer.

When a match is found, self is set and the loop ends. Otherwise, nil is returned for the call.

Below, are two working and two failing examples:

let myOtherEnum = MyOtherEnum(caseName:"xxx")
print(myOtherEnum) // MyOtherEnum.xxx

let myTestEnum = MyTestEnum(caseName:"eight")
print(myTestEnum?.rawValue) // 8

let myOtherEnumFail = MyOtherEnum(caseName:"aaa")
print(myOtherEnumFail) // nil

let myTestEnumFail = MyTestEnum(caseName:"ten")
print(myTestEnumFail) // nil

Overriding Enum init?(rawValue: String) to not be optional

The default initializer is failable. It means that if the received parameter does not match a valid enum case it does return nil.

Now you want to do 2 incompatibles things:

  1. You want to redefine the default initializer making it not failable. In fact you want a default enum value created when the received param is not valid.
  2. Inside your redefined initializer you want to call a failable initializer (which no longer exists) using the same name of the new one.

This is not possible, I the 3 possible solutions as follows:

  1. Creating a different init

You define a new not failable initializer with a default value, a different parameter name and inside it you call the default failable initializer.

enum Language: String {

case english = "English", italian = "Italian", french = "French"

init(fromRawValue: String) {
self = Language(rawValue: fromRawValue) ?? .english
}
}

  1. Redefining the default init

You redefine the default initializer, you make it not failable and you write the full logic inside it.

enum Language: String {

case english = "English", italian = "Italian", french = "French"

init(rawValue: String) {
switch rawValue {
case "Italian": self = .italian
case "French": self = .french
default: self = .english
}
}
}

  1. Creating a static func
enum Language: String {

case english = "English", italian = "Italian", french = "French"

static func build(rawValue: String) -> Language {
return Language(rawValue: rawValue) ?? .english
}
}

Now you can build a Language value writing:

let italian = Language.build(rawValue: "Italian") // Italian
let defaultValue = Language.build(rawValue: "Wrong input") // English

Enum initialized with a non-existent rawValue does not fail and return nil

I filed a bug with Apple and this is the reply I received:

"Engineering has determined that this issue behaves as intended based on the following information:

Because C enums may have values added in future releases, or even have "private case" values used by the framework that are not included in the headers, there's no way to check whether a value provided in Swift is actually valid or invalid. Therefore, init(rawValue:) is obliged to produce a value just as a C cast would. There are discussions in the Swift Open Source project on how to improve this situation in later versions of Swift, but the initializer for MKMapType still won't return nil."

Thanks to Apple Engineering for this explanation.

Enum not working in custom initializer

This is a pretty weird bug

It occurs when you attempt to pass an enum into an argument of an initialiser, autocomplete will fail and instead of suggesting the enum cases after typing Enum., it will list the instance members of the class you’re calling the initialiser on. If you try and use the single dot syntax (.Case), autocomplete will also fail, but instead of displaying the list of instance members, it simply won't display anything.

I initially thought it may have had something to do with the naming of your enum and class (BuildingType & Building), but this is not the case.

This bug only appears to be present in initialisers with multiple arguments (one of which being an enum). I couldn’t reproduce this issue with single argument initialisers.

The reproducibility appears to depend on whether the initialiser is 'complete'. I'm considering an initialiser to be 'complete' if it has all argument names and values (except the enum) defined. For example:

// Incomplete (foo is one argument of many)
let baz = Baz(foo: Foo.

// Semi-Complete (before you assign the second parameter a value)
let baz = Baz(foo: Foo., string: <String Placeholder>)

// Complete
let baz = Baz(foo: Foo., string: "")

// Complete (note the lack of the last bracket)
let baz = Baz(param: 0, foo: Foo.

Here is my test setup (Xcode 7.3, Swift 2.2):

enum Foo {
case Bar
}

class Baz {
var iReallyShouldntBeDisplayedHere = 0
init(foo:Foo, string:String) {}
init(foo: Foo) {}
}

And here is a list of cases where I've found the bug does & doesn't occur:

// Enum is the only argument

// CORRECT: accepting the initialiser's autocomplete (so it's 'complete'), then typing "Foo." brings up autocomplete options for enum cases
let baz = Baz(foo: Foo.)

// CORRECT: typing the initialiser yourself (so it's 'incomplete'), then typing "Foo." in the first parameter brings up autocomplete options for enum cases
let baz2 = Baz(foo: Foo.

// Enum is one argument of many

// INCORRECT: accepting the initialiser's autocomplete (so it's 'semi-complete'), then typing "Foo." in the first parameter brings up Baz's instance members ("iReallyShouldntBeDisplayedHere")
let baz3 = Baz(foo: Foo., string: <String Placeholder>)

// CORRECT: typing the initialiser yourself (so it's 'incomplete'), and typing "Foo." in the first parameter brings up enum cases
let baz4 = Baz(foo: Foo.

// Single dot syntax (where enum is one argument of many)

// CORRECT: typing the initialiser yourself (so it's 'incomplete'), and typing "." in the first parameter brings up enum cases
let baz5 = Baz(foo:.

// CORRECT: accepting the initialiser's autocomplete (so it's 'semi-complete'), then typing "." in the first parameter brings up enum cases
let baz6 = Baz(foo:., string: <String Placeholder>)

// INCORRECT: modifying the foo: argument once the initialiser is 'complete' by typing "." in the first parameter doesn't generate the autocomplete list
let baz7 = Baz(foo:., string: "")

I also tried this where foo: is the last argument, but the initialiser always has to be complete in that case, so it always fails. I did try with initialisers that take 3 arguments, but it appears to have the same behaviour as an initialiser with 2 arguments.

If anyone knows of any more cases where this bug can be reproduced, I'd love to know!

Swift enum with a custom constructor

You don't need to return anything. You just call initializer:

enum MyEnum: String {
case hello = "hello"
case world = "world"

init?(caseInsensitive string: String) {
self.init(rawValue: string.lowercased())
}
}

print(MyEnum(caseInsensitive: "HELLO") as Any) // => Optional(Untitled.MyEnum.hello)
print(MyEnum(caseInsensitive: "Goodbye") as Any) // => nil

How to prevent a failable initializer of a Swift enum from returning nil within a range of raw numbers?

After all, it was Code Different that threw me in the right direction. Indeed, there is no other way than turning MapColor into a struct but with a twist: I kept the former enum and renamed it RawColor, so that I can still use explicit color cases and I do not have to remember the exact raw values for a given color in the color map. Thus, for those of you who may have a similar problem to mine now or in the future, I post here the code that was tested in a playground (Xcode 8 GM using Swift 3.0):

import simd

enum RawColor:UInt8
{
case black = 0
case white = 1
case red = 2
case orange = 7
case yellow = 16
case gray = 30
case darkBrown = 40
case lightBrown = 58
case blue = 59
case green = 67
case lightGreen = 77
case rampBlue = 78
case rampGreenBlue = 123
case rampBlueGreen = 128
case rampDarkGreen = 134
case rampGreen = 142
case rampLightGreen = 166
case rampYellow = 185
case rampLightOrange = 202
case rampOrange = 220
case rampDarkOrange = 231
case rampRedOrange = 238
case rampRed = 255
}

struct MapColor
{
var rawValue:UInt8

init(value:UInt8)
{
self.rawValue = value
}

init(color:RawColor)
{
self.rawValue = color.rawValue
}

static func random() -> MapColor
{
return MapColor(value: UInt8(abs(random(maximum: 255))))
}

private static func random(maximum:Double) -> Double
{
let r = Int32(Int64(arc4random()) - Int64(RAND_MAX))
let v = (Double(r) / Double(RAND_MAX)) * maximum
return v
}
}

// specify a map color with a specific value
let red = RawColor.red
let mapRed = MapColor(color: red)
let value = mapRed.rawValue // value = 2

// random colors
for i in 0..<256
{
let randomColor = MapColor.random()
let rawValue = randomColor.rawValue
}

Can you access @Environment values in a custom struct initializer in Swift/SwiftUI?

The answer is no, you can't access the environment in the initializer, but that's not necessary. You can access the environment values without assigning them to your View struct.

struct ButtonBack: View {
@Environment(\.presentationMode) private var presentationMode

var body: some View {
Button(action: self.dismiss) {
// some View
}
}

private func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
}


Related Topics



Leave a reply



Submit