While working on a project you will have to deal with UI customization. and if you are working in a team you need to take into consideration that the UI customization will be used across the project by different developers. So to make sure that your customization works well from an API design perspective you will need to assure that your code assures those 5 points:
- Ease of use
- Discoverability
- Capabilities
- Domain definition
- Future proof
In my case, I don’t have a real complicated UI customization, instead, it is a really simple one, but it shows how important following the API design specification is.
Flatten Button
This is what will call my customized button, it will have a new title and a loading indicator, and it will get updated based on the currentState
.
Before we begin simply this is how it will look like:
The FlattenButton
will inherit UIButton
. By doing so, we will have all the functionality of the UIButton and we can override and customize what we need instead of building from scratch a button with different interactions.
State
The State
in an enum type, we will be using it in FlattenButton
to control and update its mode.
enum State { case regular case loading}
Regular
When the state is regular we’re going to display only a UILabel
centered in the middle with custom padding
Loading
When it is loading, we need to display a custom loading indicator and update the UILabel
position
Updating the title’s text & color
As you know the UIButton
already natively have
func setTitle(_: String?, for: UIControl.State)
&
func setTitleColor(_: UIcolor?, for: UIControl.State)
and, at the same time, we want to update the title’s text and color based on the button's current state. Todo so I created 2 functions with the same naming but instead of having a state of a type UIControl.State
we’re going to use FlattenButton.State
. But, how we can make sure the developers are going to use those 2 new functions. well, with the help of the deprecated annotation, the autocompletion of these functions will be greyed out, and when the developer tries to use the native function he will get a warning message tells him that this function is renamed to it is corresponding with FlattenButton.State
@available(*, deprecated, renamed: “setTitleColor(color:loadingState:)”)
Update the UI
I want to note that in this project I’m using
SnapKit
to make AutoLayout easy.
The button on the first initialization will have currentState = .regular
by default, and the developer can change the state simply by calling
flattenButton.currentState = .loading
so we need to have some KVO on the currentState
like this
Whenever the value of the currentState
gets changed we will call the validateState()
which is going to update the UI accordingly.
if the currentState
is regular
, we need to make that the spinner is hidden, and set the text for the regular
state.
To keep a record for the normalText
, color
, loadingText
and color
we need to define a new model StateContext
a new struct that has title and color properties
We are going to have private 2 properties inside the FlattenButton
to record the context for each state, and when the states’ value gets changed we update the UI and the context accordingly using those 2 properties.
Let us check how we are going to update the context:
so Simple right?
what is left is the constraints update
Constraints
As I will be using SnapKit, you will be seeing a SnapKit script.
As you can tell from the code above, I’m removing the instance of the spinner when I want to display the regular
mode, and re-add it when it is loading
.
And for the UILabel
, I had to remake the constraints according to the positing and the state.
Why not updateConstraints
?
SnapKit has an issue with updateConstraints
where it leads to crash on that version so this was the only I could do it back then or use native constant layout 😅.
And voilà we’re done creating FlattenButton
. Now let us see how we’re going to use it:
As this is just for test purposes, I’m deploying for iOS 15 so I’m using configuration, but for the older version, you will need to deal with the button as in the old days.
calling setTitle:
animatedFlattenButton.setTitle(“Hold tight!”, for: .loading)
animatedFlattenButton.setTitle(“Press Me”, for: .regular)
Simply, similar to the native function, and with the deprecated annotation, the developer will never mistake which function he needs to use to make sure it will work properly
Changing state
I’m toggling the state based on the old value, but you get it.
animatedFlattenButton.currentState = animatedFlattenButton.currentState == .loading ? .regular : .loading
A straightforward implementation and easy to maintain, which will make all the API design points fulfilled.
Again it is a really simple project, A POC so you understand the points that you need to make sure of when you create your next APIs. I know when you work on a real project creating APIs is a pain in 🍑
To know more about how I Created the custom Spinner check it out here
You find the full project by clicking on Github. Don’t forget to hit follow on Twitter if you liked this Article it encourages me to write more ❤️