Monitoring Screen Brightness Using Combine
Leveraging the power of Combine to observe changes in screen brightness making barcodes & QR codes more easily scannable.
This article assumes some basic knowledge of Swift programming as well as of the Combine framework. Namely, it assumes you know what Publishers are and why you might use them.
Apps which present barcodes or QR codes to be scanned frequently raise the screen brightness to maximum when presenting the code to allow it to be more easily scanned. This means that the member of staff scanning the QR code doesn’t need to instruct a customer to manually raise the screen brightness when scanning difficulties are encountered.
For example, we may wish to create a screen containing a QR code that when presented raises the screen brightness to full — and then decreases the screen brightness to the original value when closed so as to avoid wasting the customer’s battery. In order to achieve this we’ll need to be able to:
- Monitor the current screen brightness in order to obtain the original screen brightness level
- Set the screen brightness in order to set to full and then back again to the original value
A Simple Implementation
Where using Swift and Combine in an iOS app we can easily write a service which will allow us to subscribe to the current screen brightness by using the publisher that is provided to us on NotificationCenter as part of Foundation framework:
let notificationCenter = NotificationCenter.defaultlet brightnessPublisher = notificationCenter.publisher(for: UIScreen.brightnessDidChangeNotification).eraseToAnyPublisher()
Here’s a simplistic initial implementation:
In the above example we’ve:
- Created a protocol for our screen brightness service — this will make it easy for us to test our code wherever we call the newly created service. All we’ll need to do is create a mock screen brightness service which conforms to the same protocol and inject that into our view model (or wherever else we choose to make use of the service).
- Defined a
typealias
for our publisher so that if we ever want to change the type of our publisher we can simply update the declaration and the type of publisher will change everywhere throughout our code. - Declared that our service publishes a stream of
Double
values. After all, it’s not very convenient to work with a stream ofNotification
objects and have to convert to a brightness value each time. - In the service implementation, we’ve mapped the NotificationCenter publisher to the type of publisher that we want i.e. one which publishes a stream of
Double
values, notNotification
objects. - Type-erased the mapped publisher by calling
eraseToAnyPublisher
. This means that any calling code that makes use of our service will receive a publisher of typeAnyPublisher
. This means that the calling code is agnostic of the publisher implementation. The key benefit of this is that allows us to change the implementation of our publisher without breaking the calling code so long as we conform to the protocol.
An important point to note is that the Notification object itself does not provide us with the current screen brightness value, it only tells us that the screen brightness has changed. Therefore we need to obtain the current screen brightness on receiving the notification by calling UIScreen.main.brightness
.
A Testable Implementation
We’ve created a protocol for our screen brightness service to conform to in order to allow us to test our calling code however the service implementation itself isn’t very unit-testable right now.
Primarily this is because we can’t inject a screen brightness value and test that our publisher publishes the correct value.
In order to fix this let’s create a protocol for UIScreen
as follows:
In the above, we’ve created the protocol ScreenBrightness
and we’ve created an extension on UIScreen
declaring conformance to it. There’s no need to add any code to the extension because UIScreen already conforms to this protocol as it already has a property called brightness
which we can get and set.
We can now pass any object that conforms to the ScreenBrightness protocol into our service e.g.
let screenBrightnessService = UIKitScreenBrightnessService2(screenBrightness: UIScreen.main)
Note that the property we’ve declared as part of the protocol will have both getters and setters. That’s because as we mentioned earlier we’ll want to be able to update the screen brightness as well as observe its current value.
In the above example, we’ve also added a method to set the screen brightness which will update the screen brightness via the newly added screenBrightness
property we’ve just added.
We could have made the property an internal variable rather than a let
constant however in this case we wanted to enforce that the calling code must receive the screen brightness by observing the published value rather than by accessing the current screen brightness directly.
Our implementation isn’t yet unit-testable because whilst we can control the screen brightness value that the service reads we can’t yet trigger a notification from our test that will indicate a change in screen brightness to the service and thus trigger the new screen brightness to be published by the service.
When we obtain a publisher for NotificationCenter
the type of that publisher is NotificationCenter.Publisher
. Ideally, we’d like our service to be agnostic to this publisher’s specific implementation. Let’s make use of eraseToAnyPublisher
again to fix this.
let notificationCenter = NotificationCenter.defaultlet notificationPublisher = notificationCenter.publisher(for: UIScreen.brightnessDidChangeNotification).eraseToAnyPublisher()
The type of notificationPublisher
is AnyPublisher<Notification, Never>
which means that the publisher we observe to be made aware of changes in screen brightness needn’t be provided by NotificationCenter any longer. All our service knows about the implementation is that it publishes Notification
objects and never produces any errors (Never
is a special type of error signifying this publisher never produces any errors).
We now pass in our notification publisher via the initializer meaning that we can now control when our service reacts to changes in screen brightness as part of a unit test.
We initialize the service as follows:
let notificationCenter = NotificationCenter.defaultlet notificationPublisher = notificationCenter.publisher(for: UIScreen.brightnessDidChangeNotification).eraseToAnyPublisher()let screenBrightnessService = UIKitScreenBrightnessService2(
notificationPublisher: notificationPublisher
screenBrightness: UIScreen.main
)
A Robust Implementation
Our implementation may now be unit-testable but there’s a slight problem here however because the publisher provided by our service will only publish values when a notification is sent i.e. when there’s a change in screen brightness.
If our goal is to observe the current screen brightness, set the brightness to maximum when our screen is displayed and then reset the brightness to the original value then we’ll need to know what the original screen brightness value was before increasing it to the maximum.
As things stand we’d only be informed of the change in screen brightness once either we update the brightness programmatically or the user updates the screen brightness via the UI.
For this publisher to be of any use we likely want to receive the current screen brightness value on subscription to the publisher. This way we’ll always know what the current screen brightness is even before we make an adjustment to it.
Without changing the protocol our service conforms to, let’s update the implementation:
In the above code, we’ve updated our implementation with a new instance variable called brightnessSubject
which is a CurrentValueSubject
. Subjects in Combine terminology are still publishers but whereas you cannot send a value to a Combine publisher (you can only subscribe to a publisher), you can send a value to a subject.
There are currently two types of subject provided by Combine:
CurrentValueSubject
— always has a current value which may be inspected at any time via thevalue
property. When you update thevalue
property, this new value is published to all subscribers. If you subscribe to a CurrentValueSubject after the value has been published, you will still receive the current value of the subject on subscription.PassthroughSubject
— you may send values to this subject which will be relayed on to any active subscribers. However, if a subscriber subscribes to this subject after a value has been sent, it will not receive this value on subscription. It will only receive a value the next time a value is sent to the subject after subscription.
In our example, we choose to use a CurrentValueSubject because it means that on subscription, the calling code will immediately receive the current screen brightness value, even if it subscribes to the subject after that value has been provided to the subject.
Notice that we have amended our initializer such that when we initialize the service, we also initialize the subject with the current screen brightness value. Any subscribers to our subject will immediately receive this value on subscription meaning that we’ll be able to store the value of the screen brightness prior to us setting it to full as part of the calling code.
Modifying our service implementation to use a subject rather than mapping the notification publisher as we did in the previous step would mean that our service would no longer conform to the protocol we declared at the start unless we add a new property as follows:
var publisher: AnyPublisher<Double, Never> { brightnessSubject.eraseToAnyPublisher()
}
By type-erasing the subject to AnyPublisher
it means that our service remains conformant to the protocol and the calling code which makes use of our service remains unaware of the underlying implementation details of our publisher.
The one glaring change we’ve made to the code that we’ve yet to discuss is that we now subscribe to the notification publisher as part of a separate function rather than putting this logic in the initializer just to neaten things up a little.
Here, we compact map the screen brightness value from a CGFloat
to a Double
. Swift 5.5 performs this conversion for us automatically but wouldn’t do so with an optional CGFloat?
hence the guard
statement and the compactMap
as our calling code isn’t interested in nil
values which should only ever occur in theory were our service to be released from memory.
The next line will assign
our screen brightness value to the value
property of our CurrentValueSubject i.e. we store the value to the subject any time the screen brightness is updated
Finally, we store the AnyCancellable
object which is returned as a result of subscribing to the notification publisher to a set. This keeps our subscription alive until our service( and thus the cancellables
property) gets released from memory.
Equally we could have simply created an instance variable which stores a single AnyCancellable
value e.g.
var cancellable: AnyCancellable?
...
cancellable = notificationPublisher.
compactMap { [weak self] _ -> Double? in
...
Then all that’s left to do is to make use of the new service as part of your calling code. If your application follows an MVVM architecture you might choose to make use of the service as part of a view model as follows:
With your view model created as above, you’d be able to inject it into your UIViewController
for the screen you wish to increase the brightness of. Then in the viewDidAppear
and viewDidDisappear
methods of your view controller you’d simply call the corresponding methods of your view model.
An example project containing all of the code listed above can be found on GitHub.
If you’re interested in implementing something similar on Android, my colleague Barry Irvine has written a post on managing screen brightness when using Jetpack Compose.