UI Context

When building a new surface we think in function of the model and context that in input to the render method invoked over time due to a user interaction or system event.

Although this is how react-inspired frameworks recommend to write UI, there are certain events, that rather than belonging solely to our data dependency, are actually a representation of a change affecting the whole hosting environment and not just the surface we are building.

The UIContext is responsible to give you access (and override when necessary) any features that belongs to the current environment such as: theming, trait collection, accessibility status and so on.

Read from the UI Component Context

As consumer of this API you are mostly interested to read the UIContext and not directly write onto it. Making sure the UIContext stays up to date is guaranteed by the infrastructure as long as your surface depends on infra view controllers (e.g. FBComponentViewController, FBSurfaceViewController).

For any relevant environment context update a new value is pushed onto the component/sections hierarchy before it gets completely reflow.

To access the context you can invoke it directly from your render function as following

Component Spec

#import <FBUIComponentContext/FBUIComponentContext.h>
...
namespace MyAwesomeComponentSpec {
CK_RENDER
CKComponent *render(const Props &props){
auto const traitCollection = CK::UIContext::getTraitCollection();
auto const theme = CK::UIContext::getTheme();
return FBLabelTextComponent({
.string = DescriptionForTraitCollection(traitCollection),
.color = FDS::UsageColor(FDS::UsageColor::PrimaryText, theme)
});
}

Components API

#import <FBUIComponentContext/FBUIComponentContext.h>
...
@implementation MyAwesomeComponent {
+ (instancetype)new
{
auto const traitCollection = CK::UIContext::getTraitCollection();
auto const theme = CK::UIContext::getTheme();
return [super newWithComponent: FBLabelTextComponent({
.string = DescriptionForTraitCollection(traitCollection),
.color = FDS::UsageColor(FDS::UsageColor::PrimaryText, theme)
});]
}
}

In case you can't rely on view controller containment API to build your surface, you'll have to manually guarantee the right UIContext instance is available accordingly to the received events.

Write onto the UI context

In case you need to override a received UIContext instance or you need to build your own integration with the UIContext, the API also supports a writer access that allows you to override the available instance that is available to the current component hierarchy to build.

Every UIContext instance lives in the scope of a component hierarchy generation. It should be generated before initializing the very first component of our hierarchy and it will automatically disappear once the hierarchy has been built (all the components have been processed their initializer/render function).

As maintainer of the UIContext for your surface you are responsible to properly react to all changes that are delivered by the system and that are relevant to what we store in the UIContext. A change in the current trait collection for instance must trigger a new component hierarchy generation (a.k.a. trigger state update or context update) by making sure the UIContext is later pushed before generation:

// Callback delivered by UIKit every time **anything** in the trait collection has changed
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
auto const uiComponentContext = [FBUIComponentContext
newWithTraitCollection:self.traitCollection
theme:self.fb_theme
accessibilityStatus:_accessibilityStatus];
// Update the hosting view current model that will be passed to the component generator function.
[_hostingView updateModel:
[[MyAwesomeModel alloc]
initWithUIComponentContext:uiComponentContext
mode:CKUpdateModeAsynchronous];
// Reflow the new UIContext The consequential update model and reload will be batched together by the infra.
[_hostingView reloadWithMode:CKUpdateModeAsynchronous];
}
#pragma mark - CKComponentProvider
static auto componentForModel:(id<NSObject> model, id<NSObject> context) -> CKComponent *
{
auto const model = CK::objCForceCast<MyAwesomeModel>(model);
// Push the UI component context prior to component generation
CK::UIContext u(model.uiComponentContext);
// Build and return your component hierarchy
return ...;
}

Accessibility Update

In order to update the UIContext to be in sync with accessibility status the best way is to have your UIViewController subclass conforms to FBAccessibilityInvalidationEventsListener and update the context in one of the listener callbacks.

Because of an issue (or a feature) with Apple Accessibility notifications the callbacks can be triggered multiple times for the same event, so adding a check before modifying the UIContext can prevent multiple reflows.

Example:

@interface MYAwesomeViewController () <FBAccessibilityInvalidationEventsListener>
...
- (void)viewDidLoad {
[super viewDidLoad];
FBAddAccessibilityInvalidationEventsListener(self);
...
}
#pragma mark - Accessibility Update
- (void)didReceiveAccessibilityInvalidation
{
if (_accessibilityEnabled != CK::Component::Accessibility::IsAccessibilityEnabled()) {
_accessibilityEnabled = CK::Component::Accessibility::IsAccessibilityEnabled();
auto const uiComponentContext = [FBUIComponentContext
newWithTraitCollection:self.traitCollection
theme:self.fb_theme
accessibilityStatus:_accessibilityStatus];
// Update the hosting view current model that will be passed to the component generator function.
[_hostingView updateModel:
[[MyAwesomeModel alloc]
initWithUIComponentContext:uiComponentContext
mode:CKUpdateModeAsynchronous];
[_hostingView updateAccessibilityStatus:_accessibilityEnabled mode:CKUpdateModeSynchronous];
}
}

Plug and play

You can quickly play with the context API through our Playground app:

$ arc playground UIContextShowcase