While thinking about personalization features that I could add to my app Drinksly, I decided that a tint picker for the app was a good idea. Giving users the freedom to make the .accentColor
their favorite color is a great way to make the Starbucks Secret Menu app feel a little more friendly.
Why an Inline Picker?
I love the look of an inline picker. It doesn’t require having to navigate to another view and selecting a value. Instead, you can pick the values in an Inline List! An Inline List picker was not easily achieved on my end and required quite a few lines of code to create.
Extending the Color Struct
At first, I considered the simple approach to making this feature work in the app: save a Color
into UserDefaults using AppStorage. In theory, this should have worked; however, there is a catch to AppStorage (and UserDefaults also) - you cant store a Color into UserDefaults because it doesn’t conform to an accepted type. This hiccup took me down a google search frenzy seeing how to make this work. I came across a solution that stores the color as Data rather than a Color type.
To allow iOS to store a Color in UserDefaults, we will create an extension on the Color struct. Add a new file to the appropriate Group in Xcode, and add this snippet of code:
extension Color: RawRepresentable {
public init?(rawValue: String) {
guard let data = Data(base64Encoded: rawValue) else {
self = .pink
return
}
do {
let color = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor ?? .systemPink
self = Color(color)
} catch {
self = .pink
}
}
public var rawValue: String {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: UIColor(self), requiringSecureCoding: false) as Data
return data.base64EncodedString()
} catch {
return ""
}
}
}
In my instance, the primary color of my app is .pink, so that’s why I resort to this color if no stored value can be unarchived.
Implementing an Environment Binding
Once we can archive and unarchive a Color into and from UserDefaults, we need a way to access this value. SwiftUI allows us to access the wrapped value of a Binding, which is great because this is what we will be using. In the same file that houses your extension, you can add this snippet of code:
struct CustomColorKey: EnvironmentKey {
static var defaultValue: Binding<Color> = Binding.constant(.pink)
}
extension EnvironmentValues {
var customColor: Binding<Color> {
get { self[CustomColorKey.self] }
set { self[CustomColorKey.self] = newValue }
}
}
Simply put, we are letting our app know that we want to create an extension of EnvironmentValue that creates a custom variable called customColor
that will be the Binding of our color. This binding will read from, and write to the CustomColorKey struct which adopts the EnvironmentKey protocol. This protocol has a static variable named defaultValue
which we will read from, and override when necessary. Whenever we want to access this customColor binding in the app, we will call it like so:
@Environment(\.customColor) private var color: Binding<Color>
To use this Environment variable in the app, we will have to pass it down into the Views. We can pass it into all subsequent views by adding the view environment modifier to the parent. Right after the calling of the parent Struct, you will add the following line of code: .environment(\.customColor, $color)
At this point in the tutorial, you might notice there’s a binding passed that you might not have (
$color
). Now is a great time to introduce the color binding.
Accessing the AppStorage
AppStorage is an amazing property wrapper for the UserDefaults class that allows you to modify values in UserDefaults without having to write verbose code. Once the value changes the view is invalidated and this allows us to redraw and rebuild our app and its views once we pick a new color. To have an AppStorage in your app you can call it like so:
@AppStorage("color") var color: Color = Color.pink
If you are prone to making mistakes when you type, you may want to use an enum that allows you to use dot notation to receive the string values rather than typing them every time.
In my case, I named my variable “color”, and the UserDefaults key that it will be saved under is also “color.” You can name this to whatever you want, but make sure that if you change it on your end you adopt it throughout your code.
Implementing a View Model
Not everyone chooses the MVVM paradigm for managing their views, but in our case, we will be using a View Model to get our color list, names, and AppStorage variable. Since MVVM is a beast of its own, we won’t go into it in this article, but we will see the code used in my app to manage and show the color picker.
class ColorPickerViewModel: ObservableObject {
@AppStorage("color") var selectedColor: Color = Color.pink
@AppStorage("colorName") var color: String = "Pink"
private var colors = [ Color.blue, Color.green, Color.yellow, Color.pink, Color.red ]
private var colorNames = [ "Blue", "Green", "Yellow", "Pink", "Red" ]
var zippedColors: [(Color, String)] { return Array(zip(colors, colorNames)) }
func convertColor(color: String) {
selectedColor = zippedColors.filter { $0.1 == color }.compactMap { $0 }[0].0
}
}
While this is a very simple View Model, we don’t need much for this feature. We have a computed property that zips both the colors and color names into a Tuple that gets placed into an Array. There is a function that converts the selected values color string and sets the selectedColor variable to the filtered result from the zippedColors array that contains the name of the color. In theory, you should be able to just set the color variable directly, but the initial value is not read at runtime.
I am not sure if this is a bug in the compiler, or if I was doing it wrong, but it didn’t show me my initial selection when viewing the picker at the beginning.
Now inside of our ColorPickerView
(or whatever you named it), we will craft it as such:
struct ChangeTintColorView: View {
@ObservedObject var viewModel: ColorPickerViewModel
var body: some View {
List {
Picker(selection: $viewModel.color, label: EmptyView()) {
ForEach(viewModel.zippedColors, id:\.1) { option in
HStack(spacing:20) {
Circle()
.fill(option.0)
.frame(width: 15, height: 15)
Text(option.1)
.fontWeight(.medium)
}.tag(option.1)
}
.onChange(of: viewModel.color, perform: { value in
viewModel.convertColor(color: value)
})
}
.pickerStyle(InlinePickerStyle())
}
.navigationBarTitle("App Tint", displayMode: .large)
.listStyle(InsetGroupedListStyle())
}
}
Notice the
.pickerStyle(InlinePickerStyle())
modifier on the Picker. This allows the picker to be in a List format rather than a secondary view, or wheel. This is the biggest part of the “Inline” portion of this article.
Our Picker takes in the View Model’s color binding as the selection (which is the string value of the color from the zipped array). We iterate over the zippedColors array in the ForEach and utilize the option that is passed into the loop to get the color and the name. Also, once the View Model’s color binding changes, we will perform the convertColor()
function from the View Model to change the selectedColor
binding inside the ObservedObject.
Now, our app properly changes the Picker UI and shows our checkmark accordingly. Tap around in the picker and watch the checkmark bounce around. What you won’t see change, however, are the colors. The final piece to the puzzle is setting the accent color in the app. To do this we will have to reference our custom EnvironmentValue from earlier. Luckily this part is easy!
Inside of the view you want to change the accent color, we will declare an Environment variable that we will use.
@Environment(\.customColor) private var color: Binding<Color>
Then we will place the appropriate modifier on the view that you wish to adopt the Picker’s chosen color.
Make sure that you keep in mind that you are accessing the binding, so to get the underlying value of that binding you have to reference the
wrappedValue
rather than the binding. This is also super simple to do:.foregroundColor(color.wrappedValue)
Conclusion
If you dotted all your i’s and crossed all your t’s you should be seeing the UI adopt the color from the Picker. This implementation does require more steps than expected to make it achievable, but if you follow along closely and make changes as you see necessary, you should be able to get an Inline Picker in no time. If you have any questions or want to connect, follow me on Twitter by clicking the link in the footer!