Manually log screen layouts in a native iOS app

By default, the Connect SDK automatically captures screen layouts when viewDidAppear is called on each view controller. For most apps this is sufficient, but there are cases where automatic capture doesn't reflect the actual state of the screen — for example, when a screen loads data asynchronously, uses animations, or displays views outside the main view hierarchy such as alerts or overlays.

This guide covers how to use the ConnectCustomEvent API to log screen layouts manually, either to supplement automatic capture or to replace it entirely.

📘

Note

Before you begin, make sure the Connect SDK is integrated into your app. For instructions, see Integrate the Connect SDK into a native iOS app (Swift) or Integrate the Connect SDK into a native iOS app (Objective-C).

When to use manual layout logging

Automatic layout logging captures the view controller at viewDidAppear time. This is not adequate when:

  • The screen loads remote data after it appears — the layout captured at viewDidAppear may be empty or incomplete
  • UI animations need to complete before the layout is meaningful
  • A UITableView or UICollectionView needs to reload its data before capture
  • Views outside the main view hierarchy are present — such as alerts, overlays, or views attached directly to the app window
  • You need to associate a custom name with a view controller that serves multiple functions

In these cases, call the logging method at the point where the screen reaches its final state.

⚠️

Important

If your view controller overrides viewDidAppear, you must call super.viewDidAppear(animated) — otherwise automatic layout logging will not fire.

Correct:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    // Custom code
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    // Custom code
}

Log a screen layout

After remote data loads

Call layout logging when your data fetch completes and the UI has been updated, rather than relying on viewDidAppear.

func requestCompleted(responseData: [AnyHashable: Any]) {
    updateUI(responseData[productKeyKey])
    hideActivityIndicator()
    ConnectCustomEvent.sharedInstance().logScreenLayout(with: self)
}
- (void)RESTRequestCompleted:(RESTRequest *)request
                responseData:(NSDictionary *)responseData
                    response:(NSHTTPURLResponse *)response {
    [self updateUI:[responseData objectForKey:[self productKeyKey]]];
    [self hideActivityIndicator];
    [[ConnectCustomEvent sharedInstance] logScreenLayoutWithViewController:self];
}

After animations or table reloads

Use the andDelay parameter to wait for animations or a UITableView reload to complete before capturing. The delay is measured in seconds.

func requestCompleted(responseData: [AnyHashable: Any]) {
    items = responseData[itemsKey]
    itemsTable.reloadData()
    hideActivityIndicator()
    ConnectCustomEvent.sharedInstance().logScreenLayout(with: self, andDelay: 0.1)
}
- (void)RESTRequestCompleted:(RESTRequest *)request
                responseData:(NSDictionary *)responseData
                    response:(NSHTTPURLResponse *)response {
    items = [responseData objectForKey:[self itemsKey]];
    [self.itemsTable reloadData];
    [self hideActivityIndicator];
    [[ConnectCustomEvent sharedInstance] logScreenLayoutWithViewController:self
                                                                 andDelay:0.1];
}

On table row selection

Screen layout logging only captures views visible on screen at the time of the call. When a UITableView has more rows than fit on screen, call layout logging when a row is selected to ensure the captured layout matches the selected item.

func tableView(_ tableView: UITableView,
               willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    ConnectCustomEvent.sharedInstance().logScreenLayout(with: self)
    return indexPath
}
- (NSIndexPath *)tableView:(UITableView *)tableView
    willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [[ConnectCustomEvent sharedInstance] logScreenLayoutWithViewController:self];
    return indexPath;
}

With alerts and overlays

Views outside the main view hierarchy — such as UIAlertController — are not captured automatically. Pass them as related views to include them in the layout capture.

@IBAction func btnSubmitFormClick(_ sender: Any) {
    let alert = UIAlertController(title: "Thank You!",
                                  message: "We will be in touch with you soon.",
                                  preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Ok", style: .default) { _ in
        ConnectCustomEvent.sharedInstance().logScreenLayout(with: self)
    })
    present(alert, animated: true)
    ConnectCustomEvent.sharedInstance().logScreenLayout(with: self,
                                                        andRelatedViews: [alert.view])
}
- (IBAction)btnSubmitFormClick:(id)sender {
    UIAlertController *alert =
        [UIAlertController alertControllerWithTitle:@"Thank You!"
                                            message:@"We will be in touch with you soon."
                                     preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *ok = [UIAlertAction actionWithTitle:@"Ok"
                                                 style:UIAlertActionStyleDefault
                                               handler:^(UIAlertAction *action) {
        [[ConnectCustomEvent sharedInstance] logScreenLayoutWithViewController:self];
    }];
    [alert addAction:ok];
    [self presentViewController:alert animated:YES completion:nil];
    [[ConnectCustomEvent sharedInstance] logScreenLayoutWithViewController:self
                                                          andRelatedViews:@[alert.view]];
}

With a custom screen name

Use the andName parameter when a view controller serves multiple functions and you want to distinguish captures by name.

ConnectCustomEvent.sharedInstance().logScreenLayout(with: self, andName: "ProductDetail-Edit")
[[ConnectCustomEvent sharedInstance] logScreenLayoutWithViewController:self
                                                              andName:@"ProductDetail-Edit"];

With an image

Use logScreenLayoutWithImage when you want to log a static image as the background for a screen layout capture rather than the live view hierarchy. During replay, the image is displayed as a screen view or under Dynamic Update if the screen has already transitioned.

ConnectCustomEvent.sharedInstance().logScreenLayout(with: image)
[[ConnectCustomEvent sharedInstance] logScreenLayoutWithImage:image];

The method returns false if the image is nil or logging fails.

Related