Monitoring Screen Brightness Using Combine

Leveraging the power of Combine to observe changes in screen brightness making barcodes & QR codes more easily scannable.

Ross Butler
Go City Engineering

--

Photo by Proxyclick Visitor Management System on Unsplash

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 of Notification 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, not Notification 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 type AnyPublisher. 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 the value property. When you update the value 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.

--

--

Ross Butler
Go City Engineering

Senior Engineering Manager @ Go City. All views are my own.