Skip to main content

iOS implementation

Let's use XCode, to write iOS code. Open XCode, by running this command from the root directory of your app:

xed ios

When workspace is opened, locate Pods project and expand it. Search for Development Pods and find NativeListPackage inside. When it's expanded, it will show all files that we created under native-list-package/ios directory.

DataItem.swift

Let's start by defining DataItem object which will be used to hold items passed from JS code:

ios/DataItem.swift
import Foundation

@objc(DataItem)
public class DataItem: NSObject {
@objc public var imageUrl: String = ""
@objc public var itemDescription: String = ""

@objc public init(imageUrl: String, itemDescription: String) {
self.imageUrl = imageUrl
self.itemDescription = itemDescription
}
}

The class (that extends from NSObject) defines two string fields and is exported to Objective-C - we will use it later when parsing data prop in Objective-C++ code.

NativeListCell.swift

To use native lists in iOS, the rows or cells needs to be defined as custom classes that extends dedicated UIKit classes - in this case UICollectionViewCell:

ios/NativeListCell.swift
import UIKit

class NativeListCell: UICollectionViewCell {
private var container = UIStackView()
private var imageView = UIImageView()
private var label = UILabel()

override init(frame: CGRect) {
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func prepareForReuse() {
super.prepareForReuse()
container.removeArrangedSubview(imageView)
container.removeArrangedSubview(label)
container.removeFromSuperview()
imageView.image = nil
}

func setupCell(with item: DataItem, placeholderImage: String) {
label.text = item.itemDescription
label.font = .systemFont(ofSize: 10)
label.textAlignment = .center

imageView.image = UIImage(systemName: placeholderImage)

container.axis = .vertical
container.spacing = 10
container.addArrangedSubview(imageView)
container.addArrangedSubview(label)
self.addSubview(container)

label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
label.widthAnchor.constraint(equalToConstant: 100),
label.heightAnchor.constraint(equalToConstant: 20)
])

imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 100),
imageView.heightAnchor.constraint(equalToConstant: 70)
])

container.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
container.centerXAnchor.constraint(equalTo: self.centerXAnchor),
container.centerYAnchor.constraint(equalTo: self.centerYAnchor),
container.widthAnchor.constraint(equalToConstant: 100),
container.heightAnchor.constraint(equalToConstant: 100)
])

self.backgroundColor = UIColor.init(red: 137 / 255, green: 204 / 255, blue: 101 / 255, alpha: 1)
self.layer.borderColor = UIColor.blue.cgColor
self.layer.borderWidth = 1
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 5)
self.layer.shadowOpacity = 0.34
self.layer.shadowRadius = 6.27
self.clipsToBounds = true

imageView.layoutIfNeeded()
self.layoutIfNeeded()
}
}

Let's break down what is happening here. Our custom UICollectionViewCell class declares 3 UI elements that will be displayed in the list. To simplify, the size of the cell is fixed and defined in the init(frame:) initializer. Those UI elements are set up and "bound" to the data inside setupCell(with:placeholderImage:). To position elements inside the cell, we will leverage layout constraints - for more on that, visit Auto Layout anchors section in Apple's docs. The last piece of code is prepareForReuse method where elements are cleaned up when cell is being recycled.

info

For learning purposes, we only use system images/icons for the image view. After completing this guide, you can work on enhancing the experience by using remote images with e.g. SDWebImage library.

RNNativeListViewViewController.swift

Next step is to create custom view controller:

ios/RNNativeListViewViewController.swift
import UIKit

@objc(RNNativeListViewViewController)
public class RNNativeListViewViewController : UIViewController {
private static let CELL_IDENTIFIER = "MyCell"
private let NUM_OF_COLUMNS = 3
private var collectionView: UICollectionView? = nil
private var layout: UICollectionViewFlowLayout = {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
return layout
}()

@objc public var data: Array<DataItem> = [] {
didSet {
self.collectionView?.reloadData()
}
}

@objc public var backgroundColor: UIColor? {
get {
return self.view.backgroundColor
}
set(newBackgroundColor) {
self.view.backgroundColor = newBackgroundColor
}
}

@objc public var placeholderImage: String = ""

@objc public func scrollToItem(_ index: Int) {
self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredVertically, animated: true)
}
}

Let's start by defining custom class that extends base UIViewController and is exported to Objective-C code. It has private fields that hold UICollectionViewFlowLayout and UICollectionView instances. To handle JS props it also declares public properties (also exported to Objective-C code). And there's scrollToItem: method which handles our scroll command.

Next step is to extend the class with UICollectionViewDataSource protocol:

ios/RNNativeListViewViewController.swift
import UIKit

extension RNNativeListViewViewController : UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.data.count
}

public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: RNNativeListViewViewController.CELL_IDENTIFIER, for: indexPath) as! NativeListCell
let item = self.data[indexPath.item]
myCell.setupCell(with: item, placeholderImage: placeholderImage)
return myCell
}

public func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}

@objc(RNNativeListViewViewController)
public class RNNativeListViewViewController : UIViewController {
// ...
}

Inside that extension, we declare details about items count, sections count (in that case we don't split data in sections, so we return 1) and the item that is rendered in specific column & row. You can take a look at the latter method - collectionView(:cellForItemAt:) - it gets new or recycled cell for a specific column & row and bounds it to data item (via setupCell(with:placeholderImage:) method that we defined earlier on the cell instance).

The last step is to handle mounting/unmounting our list element when the view controller is displayed or disappears:

ios/RNNativeListViewViewController.swift
// ...

@objc(RNNativeListViewViewController)
public class RNNativeListViewViewController : UIViewController {
private static let CELL_IDENTIFIER = "MyCell"
private let NUM_OF_COLUMNS = 3
private var collectionView: UICollectionView? = nil
private var layout: UICollectionViewFlowLayout = {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
return layout
}()

override public func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()

guard let collectionView = self.collectionView else {
return
}

guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}

let sectionInsetMargins = layout.sectionInset.left + layout.sectionInset.right
let safeAreaMargins = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right
let marginsAndInsets = sectionInsetMargins + safeAreaMargins + layout.minimumInteritemSpacing * CGFloat(NUM_OF_COLUMNS - 1)
let itemWidth = (collectionView.bounds.size.width - marginsAndInsets) / CGFloat(NUM_OF_COLUMNS)
layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
}

public override func didMove(toParent parent: UIViewController?) {
if parent != nil {
let collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.register(NativeListCell.self, forCellWithReuseIdentifier: RNNativeListViewViewController.CELL_IDENTIFIER)
collectionView.backgroundColor = .init(white: 1, alpha: 0)

self.collectionView = collectionView
self.view.addSubview(collectionView)

collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: self.view.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
}

public override func willMove(toParent parent: UIViewController?) {
if parent == nil {
self.collectionView?.removeFromSuperview()
self.collectionView = nil
}
}

@objc public var data: Array<DataItem> = [] {
didSet {
self.collectionView?.reloadData()
}
}

@objc public var backgroundColor: UIColor? {
get {
return self.view.backgroundColor
}
set(newBackgroundColor) {
self.view.backgroundColor = newBackgroundColor
}
}

@objc public var placeholderImage: String = ""

@objc public func scrollToItem(_ index: Int) {
self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredVertically, animated: true)
}
}

Three methods on our view controller are overriden here.

Inside didMove(toParent:), when the view controller is mounted, the UICollectionView instance is created with the flow layout. It has NativeListCell class registered, its dataSource field is set to self and the collection view position inside view controller is set.

The willMove(toParent:) is used to do the cleanup (when the view controller is unmounted) - the collection view is unmounted and garbage collected.

Third method (viewWillLayoutSubviews) is used to declare the item size based on the width of the list element and number of columns (to simplify the example, it's set to 3 - after finishing the guide if you want, you can think how to make it dynamic and controlled from JS code).

Complete RNNativeListViewViewController.swift file
import UIKit

extension RNNativeListViewViewController : UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.data.count
}

public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: RNNativeListViewViewController.CELL_IDENTIFIER, for: indexPath) as! NativeListCell
let item = self.data[indexPath.item]
myCell.setupCell(with: item, placeholderImage: placeholderImage)
return myCell
}

public func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}

@objc(RNNativeListViewViewController)
public class RNNativeListViewViewController : UIViewController {
private static let CELL_IDENTIFIER = "MyCell"
private let NUM_OF_COLUMNS = 3
private var collectionView: UICollectionView? = nil
private var layout: UICollectionViewFlowLayout = {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
return layout
}()

override public func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()

guard let collectionView = self.collectionView else {
return
}

guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}

let sectionInsetMargins = layout.sectionInset.left + layout.sectionInset.right
let safeAreaMargins = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right
let marginsAndInsets = sectionInsetMargins + safeAreaMargins + layout.minimumInteritemSpacing * CGFloat(NUM_OF_COLUMNS - 1)
let itemWidth = (collectionView.bounds.size.width - marginsAndInsets) / CGFloat(NUM_OF_COLUMNS)
layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
}

public override func didMove(toParent parent: UIViewController?) {
if parent != nil {
let collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.register(NativeListCell.self, forCellWithReuseIdentifier: RNNativeListViewViewController.CELL_IDENTIFIER)
collectionView.backgroundColor = .init(white: 1, alpha: 0)

self.collectionView = collectionView
self.view.addSubview(collectionView)

collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: self.view.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
}

public override func willMove(toParent parent: UIViewController?) {
if parent == nil {
self.collectionView?.removeFromSuperview()
self.collectionView = nil
}
}

@objc public var data: Array<DataItem> = [] {
didSet {
self.collectionView?.reloadData()
}
}

@objc public var backgroundColor: UIColor? {
get {
return self.view.backgroundColor
}
set(newBackgroundColor) {
self.view.backgroundColor = newBackgroundColor
}
}

@objc public var placeholderImage: String = ""

@objc public func scrollToItem(_ index: Int) {
self.collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredVertically, animated: true)
}
}

RNNativeListViewContainerView.swift

Next step is to embed the view controller inside bridged view. In order to handle such case, we will use container view, that will hold the underlying view of our view controller:

ios/RNNativeListViewContainerView.swift
import UIKit

@objc(RNNativeListViewContainerView)
public class RNNativeListViewContainerView : UIView {
private var internalViewController: RNNativeListViewViewController? = nil

@objc public var viewController: RNNativeListViewViewController? {
get {
return internalViewController
}
set(newViewController) {
unmountViewController()
self.internalViewController = newViewController
if newViewController != nil {
mountViewController()
}
}
}

override public func removeFromSuperview() {
unmountViewController()
super.removeFromSuperview()
}

override public func willMove(toWindow window: UIWindow?) {
if window == nil {
unmountViewController()
} else {
mountViewController()
}
}

private func mountViewController() {
guard let viewController = viewController else {
return
}

if viewController.parent != nil {
return
}

guard let reactViewController = self.reactViewController() else {
return
}

reactViewController.addChild(viewController)
self.addSubview(viewController.view)

viewController.view.translatesAutoresizingMaskIntoConstraints = false;
NSLayoutConstraint.activate([
viewController.view.topAnchor.constraint(equalTo: self.topAnchor),
viewController.view.leadingAnchor.constraint(equalTo: self.leadingAnchor),
viewController.view.trailingAnchor.constraint(equalTo: self.trailingAnchor),
viewController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])

viewController.didMove(toParent: reactViewController)
}

private func unmountViewController() {
guard let viewController = viewController else {
return
}

if viewController.parent == nil {
return
}

viewController.willMove(toParent: nil)
viewController.view.removeFromSuperview()
viewController.removeFromParent()
}
}

The container view is a subclass of UIView that is exported to Objective-C code. It has viewController property, which is mounted or unmounted at the same time when the container view is. You can take a look at mountViewController & unmountViewController methods. These are the places where our custom view controller has its lifecycle synchronized with the container view (viewController.didMove(toParent: reactViewController) & viewController.willMove(toParent: nil)). The self.reactViewController() returns a parent view controller that holds the container view and will hold our custom view controller. The view of the RNNativeListViewViewController is also positioned with layout constraints.

info

If you plan to bridge multiple custom view controllers, the container view part can be refactored to be more generic and shared for all possible view controllers.

Now let's connect everything inside view manager and Fabric component view.

RNNativeListViewManager.h

RNNativeListViewManager.h
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>

@class RNNativeListViewContainerView;
@class RNNativeListViewViewController;

@interface RNNativeListViewManager : RCTViewManager

@end

We declare the view manager class that extends RCTViewManager. One thing you may have noticed is RCTUIManager import - we will use it to implement native commands for the old architecture view.

Also to use Swift classes, we need to do "forward-declaration" (check out Apple's Swift-ObjC interop dedicated docs section).

RNNativeListViewManager.mm

RNNativeListViewManager.mm
#import "RNNativeListViewManager.h"

#import <React/RCTConvert.h>

#import "NativeListPackage-Swift.h"

@implementation RNNativeListViewManager

RCT_EXPORT_MODULE(RNNativeListView)

RCT_CUSTOM_VIEW_PROPERTY(data, NSArray, RNNativeListViewContainerView)
{
NSArray<NSDictionary *> *array = [RCTConvert NSDictionaryArray:json];
NSMutableArray<DataItem *> *data = [NSMutableArray arrayWithCapacity:array.count];
for (int i = 0; i < array.count; i++) {
[data addObject:[[DataItem alloc] initWithImageUrl:array[i][@"imageUrl"] itemDescription:array[i][@"description"]]];
}
[view.viewController setData:data];
}
RCT_CUSTOM_VIEW_PROPERTY(options, NSDictionary, RNNativeListViewContainerView)
{
[view.viewController setPlaceholderImage:[RCTConvert NSString:json[@"placeholderImage"]]];
}
RCT_CUSTOM_VIEW_PROPERTY(backgroundColor, UIColor, RNNativeListViewContainerView)
{
[view.viewController setBackgroundColor:[RCTConvert UIColor:json]];
}

#if RCT_NEW_ARCH_ENABLED
#else
RCT_EXPORT_METHOD(scrollToItem:(nonnull NSNumber*) reactTag index:(NSInteger) index) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[RNNativeListViewContainerView class]]) {
return;
}
[((RNNativeListViewContainerView *) view).viewController scrollToItem:index];
}];
}

- (UIView *)view
{
RNNativeListViewContainerView *view = [RNNativeListViewContainerView new];
view.viewController = [RNNativeListViewViewController new];
return view;
}
#endif

@end

And as for every view manager class, we start with RCT_EXPORT_MODULE macro and we declare exported properties with RCT_EXPORT_VIEW_PROPERTY macro.

For the old architecture mode, we have to additionaly declare native command for scrollToItem method (using RCT_EXPORT_METHOD macro and RCTUIManager class).

The view getter is also declared for the old arch - for new arch we are just using Fabric component view.

RNNativeListViewComponentView.h

ios/RNNativeListViewComponentView.h
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>

@class RNNativeListViewContainerView;
@class RNNativeListViewViewController;

@interface RNNativeListViewComponentView : RCTViewComponentView

@end

#endif

Inside the header file for Fabric component, we declare the RNNativeListViewComponentView class that extends RCTViewComponentView. Additionally, we make "forward-declaration" for RNNativeListViewViewController and RNNativeListViewContainerView classes (check out Apple's Swift-ObjC interop dedicated docs section).

RNNativeListViewComponentView.mm

The boilerplate for Fabric component's implementation part will look like following:

ios/RNNativeListViewComponentView.mm
#if RCT_NEW_ARCH_ENABLED
#import "RNNativeListViewComponentView.h"

#import <React/RCTConversions.h>
#import <RCTTypeSafety/RCTConvertHelpers.h>

#import <react/renderer/components/NativeListPackage/ComponentDescriptors.h>
#import <react/renderer/components/NativeListPackage/EventEmitters.h>
#import <react/renderer/components/NativeListPackage/Props.h>
#import <react/renderer/components/NativeListPackage/RCTComponentViewHelpers.h>

#import "RCTFabricComponentsPlugins.h"

using namespace facebook::react;

@interface RNNativeListViewComponentView () <RCTRNNativeListViewViewProtocol>
@end

@implementation RNNativeListViewComponentView

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNNativeListViewProps>();
_props = defaultProps;
}

return self;
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RNNativeListViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RNNativeListViewProps>(props);

[super updateProps:props oldProps:oldProps];
}

- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRNNativeListViewHandleCommand(self, commandName, args);
}

- (void)scrollToItem:(NSInteger)index
{
//
}

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNNativeListViewComponentDescriptor>();
}

@end

Class<RCTComponentViewProtocol> RNNativeListViewCls(void)
{
return RNNativeListViewComponentView.class;
}
#endif

At the top there are new arch imports and conversion helpers. The component extends code-generated protocol that declare the native commands methods from the JS spec. Next we implement all required methods and create RNNativeListViewCls function.

As a next part, let's initialize the container view with our view controller:

ios/RNNativeListViewComponentView.mm
//...

#import "NativeListPackage-Swift.h"

using namespace facebook::react;

@interface RNNativeListViewComponentView () <RCTRNNativeListViewViewProtocol>
@end

@implementation RNNativeListViewComponentView

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNNativeListViewProps>();
_props = defaultProps;

RNNativeListViewContainerView *view = [RNNativeListViewContainerView new];
view.viewController = [RNNativeListViewViewController new];

self.contentView = view;
}

return self;
}

// ...

@end

// ...

Next step is props handling:

ios/RNNativeListViewComponentView.mm
//...

@implementation RNNativeListViewComponentView

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNNativeListViewProps>();
_props = defaultProps;

RNNativeListViewContainerView *view = [RNNativeListViewContainerView new];
view.viewController = [RNNativeListViewViewController new];

self.contentView = view;
}

return self;
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RNNativeListViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RNNativeListViewProps>(props);

RNNativeListViewContainerView *view = (RNNativeListViewContainerView *)self.contentView;

auto dataComparator = [](const RNNativeListViewDataStruct &left, const RNNativeListViewDataStruct &right) {
return left.imageUrl == right.imageUrl && left.description == right.description;
};

if (!std::equal(oldViewProps.data.begin(), oldViewProps.data.end(), newViewProps.data.begin(), newViewProps.data.end(), dataComparator)) {
NSArray *data = RCTConvertVecToArray(newViewProps.data, ^(RNNativeListViewDataStruct item){
DataItem *dataItem = [[DataItem alloc] initWithImageUrl:RCTNSStringFromString(item.imageUrl) itemDescription:RCTNSStringFromString(item.description)];
return dataItem;
});
[view.viewController setData:data];
}

if (oldViewProps.options.placeholderImage != newViewProps.options.placeholderImage) {
[view.viewController setPlaceholderImage:RCTNSStringFromString(newViewProps.options.placeholderImage)];
}

if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
UIColor *backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor);
[view.viewController setBackgroundColor:backgroundColor];
}

[super updateProps:props oldProps:oldProps];
}

// ...

@end

// ...

Here we are handling 3 props - data, options.placeholderImage & backgroundColor (from style prop).

The data prop is quite interesting, it's array, so we need to compare the old and new value of that array. To do that in Objective-C++, we will use C++ std::equal function. It takes the ranges of arrays and comparator function that we declare under dataComparator variable.

info

To learn more about anonymous function in C++ check Lambda expressions section in C++ reference

The last thing left is to implement scrollToItem: method:

ios/RNNativeListViewComponentView.mm
//...

@implementation RNNativeListViewComponentView

//...

- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRNNativeListViewHandleCommand(self, commandName, args);
}

- (void)scrollToItem:(NSInteger)index
{
RNNativeListViewContainerView *view = (RNNativeListViewContainerView *)self.contentView;
[view.viewController scrollToItem:index];
}

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNNativeListViewComponentDescriptor>();
}

@end

// ...
Complete RNNativeListViewComponentView.mm file
#if RCT_NEW_ARCH_ENABLED
#import "RNNativeListViewComponentView.h"

#import <React/RCTConversions.h>
#import <RCTTypeSafety/RCTConvertHelpers.h>

#import <react/renderer/components/NativeListPackage/ComponentDescriptors.h>
#import <react/renderer/components/NativeListPackage/EventEmitters.h>
#import <react/renderer/components/NativeListPackage/Props.h>
#import <react/renderer/components/NativeListPackage/RCTComponentViewHelpers.h>

#import "RCTFabricComponentsPlugins.h"

#import "NativeListPackage-Swift.h"

using namespace facebook::react;

@interface RNNativeListViewComponentView () <RCTRNNativeListViewViewProtocol>
@end

@implementation RNNativeListViewComponentView

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNNativeListViewProps>();
_props = defaultProps;

RNNativeListViewContainerView *view = [RNNativeListViewContainerView new];
view.viewController = [RNNativeListViewViewController new];

self.contentView = view;
}

return self;
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RNNativeListViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RNNativeListViewProps>(props);

RNNativeListViewContainerView *view = (RNNativeListViewContainerView *)self.contentView;

auto dataComparator = [](const RNNativeListViewDataStruct &left, const RNNativeListViewDataStruct &right) {
return left.imageUrl == right.imageUrl && left.description == right.description;
};

if (!std::equal(oldViewProps.data.begin(), oldViewProps.data.end(), newViewProps.data.begin(), newViewProps.data.end(), dataComparator)) {
NSArray *data = RCTConvertVecToArray(newViewProps.data, ^(RNNativeListViewDataStruct item){
DataItem *dataItem = [[DataItem alloc] initWithImageUrl:RCTNSStringFromString(item.imageUrl) itemDescription:RCTNSStringFromString(item.description)];
return dataItem;
});
[view.viewController setData:data];
}

if (oldViewProps.options.placeholderImage != newViewProps.options.placeholderImage) {
[view.viewController setPlaceholderImage:RCTNSStringFromString(newViewProps.options.placeholderImage)];
}

if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
UIColor *backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor);
[view.viewController setBackgroundColor:backgroundColor];
}

[super updateProps:props oldProps:oldProps];
}

- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRNNativeListViewHandleCommand(self, commandName, args);
}

- (void)scrollToItem:(NSInteger)index
{
RNNativeListViewContainerView *view = (RNNativeListViewContainerView *)self.contentView;
[view.viewController scrollToItem:index];
}

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNNativeListViewComponentDescriptor>();
}

@end

Class<RCTComponentViewProtocol> RNNativeListViewCls(void)
{
return RNNativeListViewComponentView.class;
}
#endif

You can check training repo for Objective-C & Swift implementation here and Objective-C-only implementation here.

That's iOS part, now let's go to Android!