1 min read

Add a Picker to the Profile

Last Updated - Platform 20.0 - SDK 15.0

This guide will walk through how to add a picker to the Profile screen using customData.

Task outline

Before we begin let's define our end goal in a few bullet points:

  • Allow the user to select from a list of titles (titles are outdated so lets use a potato related list).
  • Show the title potato picker on the Profile below the name input views.
  • Populate our potato picker from the custom data object.
  • Send our potato value to the API when updating the profile.

We're going to assume the backend contract for customData is as follows:

{ "potato": "boiled" }

Present the picker

  1. Create a new file named ProfileView.swift and save it in a good location (Sources/Account/Views).
  2. Begin by subclassing the PoqProfileView here and implement PickerInputViewDelegate.
class CustomProfileView: PoqProfileView, PickerInputViewDelegate {
override var formViews: [UIView] {
var views = super.formViews
// Handle presenting the picker here.
return views
}
override func setup(with viewData: ProfileViewData) {
super.setup(with: viewData)
// Handle setting the initial picker state here.
}
func pickerInputView(_ view: PickerInputView, didSelect item: PickerItemViewData?) {
// Handle user interaction here.
}
}
  1. Now that we've got our skeleton, let's add a potato picker to our ProfileView.
lazy var potatoPicker: PickerInputView = {
let view = Self.container.views.pickerInputView()
view.title = "Potato"
view.hint = "Pick a potato style"
view.items = [
.init(id: "boiled", title: "Boil 'em"),
.init(id: "mashed", title: "Mash 'em"),
.init(id: "stewed", title: "Stick 'em in a stew")
]
view.delegate(to: self)
return view
}()
override var formViews: [UIView] {
var views = super.formViews
views.insert(potatoPicker, at: 1) // Insert after the name container.
return views
}
  1. Head over to your AppModule to your setUpAccount function and add the following to inject our custom ProfileView.
func setUpAccount() {
Container.shared.views.profileView = { CustomProfileView() }
...
}
  1. Now let's make it work! Create a new file named CustomProfileViewData.swift in a good location (Sources/Account/Models) and add the following matching our backend contract.
struct CustomProfileViewData: Hashable {
var potato: String?
}
  1. Head back to our ProfileView and implement the setup and pickerDidSelect functions.
override func setup(with viewData: ProfileViewData) {
super.setup(with: viewData)
// Preselect an item with an id matching 'customData.potato'.
potatoPicker.selected = viewData.customData(CustomProfileViewData.self)?.potato
.flatMap(potatoPicker.items.first)
}
func pickerInputView(_ view: PickerInputView, didSelect item: PickerItemViewData?) {
var customData = viewData.edited?.customData(CustomProfileViewData.self) ?? .init()
customData.potato = item?.id
viewData.edited?.customData = customData
viewData.edited?.isValid = isValid
}
  1. Great! You can build and run and should see the new potato input.

Communicating the data

Finally we need to handle mapping to and from the data layer (which uses AnyCodable).

To do this it's always a good idea to create a separate model for your data layer to match the BE contract. Let's tweak our scenario to demonstrate why this is a good idea.

Let's imagine our API development was outsourced, and unfortunately there was a naming mishap. The backend only accepts and understands tattie. But people may not know what that is; 'potato' is better.

  1. Create a new file named CustomProfileData.swift in a good location (Sources/Account/Models) and match the new backend contract.
struct CustomProfileData: Codable, Hashable {
var tattie: String?
}
  1. Create another new file named ProfileMapper.swift in a good location (Sources/Account/Models) and conform to ProfileMapper.
struct CustomProfileMapper: ProfileMapper {
func map(from data: ProfileResponse) -> Profile {}
func map(from domain: Profile) -> ProfileRequest {}
}
  1. We can use decoration to retain all the functionality of the SDK to benefit from future fixes whilst reducing the change of breaking changes. With this it's easy to see what we are customising.
lazy var decorated = Container.poq.mappers.profileMapper()
func map(from data: ProfileResponse) -> Profile {
var profile = decorated.map(from: data)
profile.customData = data.customData?.decode(CustomProfileData.self).flatMap {
CustomProfileViewData(potato: $0.tattie)
}
return profile
}
func map(from domain: Profile) -> ProfileRequest {
var data = decorated.map(from: domain)
data.customData = domain.customData(CustomProfileViewData.self).flatMap {
AnyCodable(CustomProfileData(tattie: $0.potato))
}
return data
}
  1. Head back to your AppModule to your setUpAccount function and add the following to inject the mapper.
func setUpAccount() {
Container.shared.mappers.profileMapper = { CustomProfileMapper() }
...
}

Now that the mapper is in place, other frontend developers will understand that our picker picks potatoes and that potatoes map to and from tatties on the backend.

So that's it! Run your app and enjoy your potatoes.