WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Using Classes, Enums, or Structs to Simplify Settings

Forums > SwiftUI

One of the features of my app is the ability to "favorite" a Core Data item.

A favorite item gets a Image(systemName: "heart.fill") and one that hasn't been favorited gets Image(systemName: "heart"). I'm doing this with a simple comparison operator, like so:

Image(systemName: item.favorited ? "heart.fill" : "heart" )
  .foregroundColor( item.favorited ? .red : .black)

I'd like to provide the option to switch from hearts to stars. (I know this isn't a dramatic or integral feature; it's more of a teaching exercise for myself to improve my skills.)

So I'd have in my app's settings the ability to choose between hearts or stars for favorites.

Obviously I'm not going to do this:

let x = hearts_or_stars_preference
Image(systemName: item.favorited ? ( x ? "heart.fill" : "star.fill" ) : ( x ? "heart" : "star" ) )
  .foregroundColor( item.favorited ? .red : .black)

I could, but that's ugly. And what happens if I decide to also give users the ability to choose between hearts, stars, or checkmarks? Checkmarks aren't going to be "checkmark" and "checkmark.fill"; they'd be "checkmark.circle" and "xmark.circle".

Surely there's a good way to make this scalable. I'd like to be able to save an integer to UserDefaults and then determine which image to display based on that. But here's where it gets a little tricky: Hearts are red, but stars are yellow. Checkmarks could be any color (although probably green). So I need to have a single integer that allows me to determine not only the image, but also the positive/negative color associated with it.

My gut is telling me that I'd use a class for this, but I fear that's because I've spent 20 years writing PHP.

Can anyone give me some advice here? Or point me to one of Paul's articles which covers this that I might have missed?

Thanks!

3      

Use an enum for something like this.

enum FavoriteIcon: Int, RawRepresentable {
    case star
    case heart
    case check

    var iconOn: String {
        switch self {
        case .star: return "star.fill"
        case .heart: return "heart.fill"
        case .check: return "checkmark.circle"
        }
    }

    var iconOff: String {
        switch self {
        case .star: return "star"
        case .heart: return "heart"
        case .check: return "xmark.circle"
        }
    }

    var iconColorOn: UIColor {
        switch self {
        case .star: return .yellow
        case .heart: return .red
        case .check: return .green
        }
    }

    var iconColorOff: UIColor {
        return .black
    }
}

And then you would do:

let x = hearts_or_stars_preference
Image(systemName: item.favorited ? x.iconOn : x.iconOff)
  .foregroundColor( item.favorited ? Color(uiColor: x.iconColorOn) : Color(uiColor: x.iconColorOff))

Since the enum conforms to RawRepresentable, you can initialize it from your UserDefaults value like so:

let intValueFromUserDefaults = //get Int from UserDefaults
let fave = FavoriteIcon(rawValue: intValueFromUserDefaults)

3      

Thanks, @roosterboy! That appears to be exactly what I need.

But how do I force it to not be optional?

Before even attempting to associate it with a UserDefaults value, I just attempted to hard-code it, like so:

    let iconstyle = FavoriteIcon(rawValue: 1)

But when I use it -- e.g. Image(systemName: self.favorited ? iconstyle.iconTrue : iconstyle.iconFalse ) -- I'm getting this:

  • Value of optional type 'FavoriteIcon?' must be unwrapped to refer to member 'iconFalse' of wrapped base type 'FavoriteIcon'
  • Chain the optional using '?' to access member 'iconFalse' only for non-'nil' base values
  • Force-unwrap using '!' to abort execution if the optional value contains 'nil'

I don't want to have to sprinkle a bunch of question marks and exclamation points all over the place. What am I doing wrong?

3      

You aren't doing anything wrong. It's a consequence of FavoriteIcon being representable as an Int. There are (in that example) only 3 cases that are valid for FavoriteIcon (corresponding to 0, 1, and 2) but Int has millions of valid values. So, if you were to do, say, let iconstyle = FavoriteIcon(rawValue: 1000), that's not a valid value and so you would get nil back as a result.

One way you could handle that would be to supply a default value for FavoriteIcon, like so:

enum FavoriteIcon: Int, RawRepresentable {
    //...

    static var `default`: FavoriteIcon { return .heart } // or whatever you want the default to be
}

Then when you assign a FavoriteIcon, you would use nil-coalescing to assign the default value if you got nil out:

let fave = FavoriteIcon(rawValue: 1000) ?? .default

And now fave is no longer an Optional!

3      

Brilliant! Thank you so much, @roosterboy. And double extra thank you for explaining the optionals bit. I never understood that about Int and now it makes sense.

3      

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.