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.
- ObjC++ & Swift
- ObjC++ only
DataItem.swift
Let's start by defining DataItem
object which will be used to hold items passed from JS code:
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
:
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.
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:
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:
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:
// ...
@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:
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.
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
#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
#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
#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:
#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:
//...
#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:
//...
@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.
To learn more about anonymous function in C++ check Lambda expressions section in C++ reference
The last thing left is to implement scrollToItem:
method:
//...
@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
DataItem.h
#import <Foundation/Foundation.h>
@interface DataItem : NSObject
@property (nonatomic, copy) NSString * _Nonnull imageUrl;
@property (nonatomic, copy) NSString * _Nonnull itemDescription;
- (instancetype)initWithImageUrl:(NSString * _Nonnull)imageUrl itemDescription:(NSString * _Nonnull)itemDescription;
@end
We start by defining DataItem
object's interface - it extends NSObject
and declares two properties (imageUrl
& itemDescription
).
DataItem.mm
#import "DataItem.h"
@implementation DataItem
- (instancetype)initWithImageUrl:(NSString *)imageUrl itemDescription:(NSString *)itemDescription
{
self = [super init];
if (self) {
_imageUrl = imageUrl;
_itemDescription = itemDescription;
}
return self;
}
@end
Next step is declaring implementation for the object.
We will use it later when parsing data
prop in Objective-C++ code
NativeListCell.h
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
:
#import <UIKit/UIKit.h>
@interface NativeListCell : UICollectionViewCell
- (void)setupCellWithItem:(DataItem *)item placeholderImage:(NSString *)placeholderImage;
@end
NativeListCell.mm
#import "NativeListCell.h"
@implementation NativeListCell {
UIStackView *container;
UIImageView *imageView;
UILabel *label;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:CGRectMake(0, 0, 100, 100)];
if (self) {
container = [UIStackView new];
imageView = [UIImageView new];
label = [UILabel new];
}
return self;
}
- (void)prepareForReuse
{
[super prepareForReuse];
[container removeArrangedSubview:imageView];
[container removeArrangedSubview:label];
[container removeFromSuperview];
imageView.image = nil;
}
- (void)setupCellWithItem:(DataItem *)item placeholderImage:(NSString *)placeholderImage
{
[label setText:item.itemDescription];
[label setFont:[UIFont systemFontOfSize:10]];
[label setTextAlignment:NSTextAlignmentCenter];
imageView.image = [UIImage systemImageNamed:placeholderImage];
[container setAxis:UILayoutConstraintAxisVertical];
[container setSpacing:10];
[container addArrangedSubview:imageView];
[container addArrangedSubview:label];
[self addSubview:container];
label.translatesAutoresizingMaskIntoConstraints = false;
[NSLayoutConstraint activateConstraints:@[
[label.centerXAnchor constraintEqualToAnchor:container.centerXAnchor],
[label.widthAnchor constraintEqualToConstant:100],
[label.heightAnchor constraintEqualToConstant:20]
]];
imageView.translatesAutoresizingMaskIntoConstraints = false;
[NSLayoutConstraint activateConstraints:@[
[imageView.widthAnchor constraintEqualToConstant:100],
[imageView.heightAnchor constraintEqualToConstant:70]
]];
container.translatesAutoresizingMaskIntoConstraints = false;
[NSLayoutConstraint activateConstraints:@[
[container.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[container.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[container.widthAnchor constraintEqualToConstant:100],
[container.heightAnchor constraintEqualToConstant:100]
]];
[self setBackgroundColor:[[UIColor alloc] initWithRed:137.0 / 255 green: 204.0 / 255 blue:101.0 / 255 alpha:1]];
self.layer.borderColor = UIColor.blueColor.CGColor;
self.layer.borderWidth = 1;
self.layer.cornerRadius = 10;
self.layer.masksToBounds = YES;
self.layer.shadowColor = UIColor.blackColor.CGColor;
self.layer.shadowOffset = CGSizeMake(0, 5);
self.layer.shadowOpacity = 0.34;
self.layer.shadowRadius = 6.27;
self.clipsToBounds = YES;
[imageView layoutIfNeeded];
[self layoutIfNeeded];
}
@end
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 whole example, the size of the cell is fixed and defined in the initWithFrame:
initializer.
Those UI elements are set up and "bound" to the data inside setupCellWithItem: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.
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.h
Next step is to create custom view controller:
#import <UIKit/UIKit.h>
#import "DataItem.h"
@interface RNNativeListViewViewController : UIViewController
@property (nonatomic, copy) NSArray<DataItem *> * _Nonnull data;
@property (nonatomic, copy) NSString * _Nonnull placeholderImage;
@property (nonatomic, strong) UIColor * _Nullable backgroundColor;
- (void)scrollToItem:(NSInteger)index;
@end
Let's start by defining custom class that extends base UIViewController
.
RNNativeListViewViewController.mm
After declaring the header interface, let's create implementation for view controller class:
#import <UIKit/UIKit.h>
#import "RNNativeListViewViewController.h"
#import "NativeListCell.h"
@implementation RNNativeListViewViewController {
NSInteger NUM_OF_COLUMNS;
UICollectionView * _Nullable collectionView;
UICollectionViewFlowLayout *layout;
}
+ (NSString *)cellIdentifier
{
return @"MyCell";
}
- (instancetype)init
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
NUM_OF_COLUMNS = 3;
layout = [UICollectionViewFlowLayout new];
layout.sectionInset = UIEdgeInsetsMake(20, 10, 10, 10);
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
}
return self;
}
- (void)setData:(NSArray<DataItem *> *)data
{
_data = data;
if (collectionView != nil) {
[collectionView reloadData];
}
}
- (UIColor *)backgroundColor
{
return self.view.backgroundColor;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
[self.view setBackgroundColor:backgroundColor];
}
- (void)scrollToItem:(NSInteger)index
{
if (collectionView != nil) {
[collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}
}
@end
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:
#import <UIKit/UIKit.h>
#import "RNNativeListViewViewController.h"
#import "NativeListCell.h"
@interface RNNativeListViewViewController () <UICollectionViewDataSource>
@end
@implementation RNNativeListViewViewController {
NSInteger NUM_OF_COLUMNS;
UICollectionView * _Nullable collectionView;
UICollectionViewFlowLayout *layout;
}
// ...
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return _data.count;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
NativeListCell *myCell = (NativeListCell *)[collectionView dequeueReusableCellWithReuseIdentifier:[RNNativeListViewViewController cellIdentifier] forIndexPath:indexPath];
DataItem *item = _data[indexPath.item];
[myCell setupCellWithItem:item placeholderImage:_placeholderImage];
return myCell;
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
@end
With 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:cellForItemAtIndexPath:
- it gets new or recycled cell for a specific column & row and bounds it to data item (via setupCellWithItem: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:
// ...
@implementation RNNativeListViewViewController {
NSInteger NUM_OF_COLUMNS;
UICollectionView * _Nullable collectionView;
UICollectionViewFlowLayout *layout;
}
// ...
- (instancetype)init
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
NUM_OF_COLUMNS = 3;
layout = [UICollectionViewFlowLayout new];
layout.sectionInset = UIEdgeInsetsMake(20, 10, 10, 10);
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
}
return self;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
if (collectionView == nil) {
return;
}
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
if (layout == nil) {
return;
}
CGFloat sectionInsetMargins = layout.sectionInset.left + layout.sectionInset.right;
CGFloat safeAreaMargins = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right;
CGFloat marginsAndInsets = sectionInsetMargins + safeAreaMargins + layout.minimumInteritemSpacing * (CGFloat)(NUM_OF_COLUMNS - 1);
CGFloat itemWidth = (collectionView.bounds.size.width - marginsAndInsets) / (CGFloat)NUM_OF_COLUMNS;
[layout setItemSize:CGSizeMake(itemWidth, itemWidth)];
}
- (void)didMoveToParentViewController:(UIViewController *)parent
{
if (parent != nil) {
UICollectionView *newCollectionView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:layout];
newCollectionView.dataSource = self;
[newCollectionView registerClass:[NativeListCell class] forCellWithReuseIdentifier:[RNNativeListViewViewController cellIdentifier]];
[newCollectionView setBackgroundColor:[[UIColor alloc] initWithWhite:1 alpha:0]];
collectionView = newCollectionView;
[self.view addSubview:collectionView];
collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[collectionView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[collectionView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[collectionView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[collectionView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
]];
}
}
- (void)willMoveToParentViewController:(UIViewController *)parent
{
if (parent == nil) {
[collectionView removeFromSuperview];
collectionView = nil;
}
}
- (void)setData:(NSArray<DataItem *> *)data
{
_data = data;
if (collectionView != nil) {
[collectionView reloadData];
}
}
// ...
@end
Three methods on our view controller are overriden here.
Inside didMoveToParentViewController:
, 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 willMoveToParentViewController:
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.mm
file
#import <UIKit/UIKit.h>
#import "RNNativeListViewViewController.h"
#import "NativeListCell.h"
@interface RNNativeListViewViewController () <UICollectionViewDataSource>
@end
@implementation RNNativeListViewViewController {
NSInteger NUM_OF_COLUMNS;
UICollectionView * _Nullable collectionView;
UICollectionViewFlowLayout *layout;
}
+ (NSString *)cellIdentifier
{
return @"MyCell";
}
- (instancetype)init
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
NUM_OF_COLUMNS = 3;
layout = [UICollectionViewFlowLayout new];
layout.sectionInset = UIEdgeInsetsMake(20, 10, 10, 10);
layout.minimumLineSpacing = 10;
layout.minimumInteritemSpacing = 10;
}
return self;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
if (collectionView == nil) {
return;
}
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
if (layout == nil) {
return;
}
CGFloat sectionInsetMargins = layout.sectionInset.left + layout.sectionInset.right;
CGFloat safeAreaMargins = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right;
CGFloat marginsAndInsets = sectionInsetMargins + safeAreaMargins + layout.minimumInteritemSpacing * (CGFloat)(NUM_OF_COLUMNS - 1);
CGFloat itemWidth = (collectionView.bounds.size.width - marginsAndInsets) / (CGFloat)NUM_OF_COLUMNS;
[layout setItemSize:CGSizeMake(itemWidth, itemWidth)];
}
- (void)didMoveToParentViewController:(UIViewController *)parent
{
if (parent != nil) {
UICollectionView *newCollectionView = [[UICollectionView alloc] initWithFrame:self.view.frame collectionViewLayout:layout];
newCollectionView.dataSource = self;
[newCollectionView registerClass:[NativeListCell class] forCellWithReuseIdentifier:[RNNativeListViewViewController cellIdentifier]];
[newCollectionView setBackgroundColor:[[UIColor alloc] initWithWhite:1 alpha:0]];
collectionView = newCollectionView;
[self.view addSubview:collectionView];
collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[collectionView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[collectionView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[collectionView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[collectionView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]
]];
}
}
- (void)willMoveToParentViewController:(UIViewController *)parent
{
if (parent == nil) {
[collectionView removeFromSuperview];
collectionView = nil;
}
}
- (void)setData:(NSArray<DataItem *> *)data
{
_data = data;
if (collectionView != nil) {
[collectionView reloadData];
}
}
- (UIColor *)backgroundColor
{
return self.view.backgroundColor;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
[self.view setBackgroundColor:backgroundColor];
}
- (void)scrollToItem:(NSInteger)index
{
if (collectionView != nil) {
[collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return _data.count;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
NativeListCell *myCell = (NativeListCell *)[collectionView dequeueReusableCellWithReuseIdentifier:[RNNativeListViewViewController cellIdentifier] forIndexPath:indexPath];
DataItem *item = _data[indexPath.item];
[myCell setupCellWithItem:item placeholderImage:_placeholderImage];
return myCell;
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
@end
RNNativeListViewContainerView.h
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.
Let's start by defining header interface for container view:
#import <UIKit/UIKit.h>
#import "RNNativeListViewViewController.h"
@interface RNNativeListViewContainerView : UIView
@property (nonatomic, strong) RNNativeListViewViewController *viewController;
@end
It will extend base UIView
class and will hold a reference to our custom view controller class.
Next let's declare the implementation.
RNNativeListViewContainerView.mm
#import "RNNativeListViewContainerView.h"
#import <React/UIView+React.h>
@implementation RNNativeListViewContainerView {
RNNativeListViewViewController * _Nullable internalViewController;
}
- (RNNativeListViewViewController *)viewController
{
return internalViewController;
}
- (void)setViewController:(RNNativeListViewViewController *)newViewController
{
[self unmountViewController];
internalViewController = newViewController;
if (newViewController != nil) {
[self mountViewController];
}
}
- (void)removeFromSuperview
{
[self unmountViewController];
[super removeFromSuperview];
}
- (void)willMoveToWindow:(UIWindow *)newWindow
{
if (newWindow == nil) {
[self unmountViewController];
} else {
[self mountViewController];
}
}
- (void)mountViewController
{
if (self.viewController == nil) {
return;
}
if (self.viewController.parentViewController != nil) {
return;
}
UIViewController *reactViewController = self.reactViewController;
if (reactViewController == nil) {
return;
}
[reactViewController addChildViewController:self.viewController];
[self addSubview:self.viewController.view];
self.viewController.view.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[self.viewController.view.topAnchor constraintEqualToAnchor:self.topAnchor],
[self.viewController.view.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[self.viewController.view.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[self.viewController.view.bottomAnchor constraintEqualToAnchor:self.bottomAnchor]
]];
[self.viewController didMoveToParentViewController:reactViewController];
}
- (void)unmountViewController
{
if (self.viewController == nil) {
return;
}
if (self.viewController.parentViewController == nil) {
return;
}
[self.viewController willMoveToParentViewController:nil];
[self.viewController.view removeFromSuperview];
[self.viewController removeFromParentViewController];
}
@end
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 ([self.viewController didMoveToParentViewController:reactViewController]
& [self.viewController willMoveToParentViewController: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.
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
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>
@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.
RNNativeListViewManager.mm
#import "RNNativeListViewManager.h"
#import <React/RCTConvert.h>
#import "RNNativeListClassicViewContainerView.h"
#import "RNNativeListClassicViewViewController.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
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
@interface RNNativeListViewComponentView : RCTViewComponentView
@end
#endif
Inside the header file for Fabric component, we declare the RNNativeListViewComponentView
class that extends RCTViewComponentView
.
RNNativeListViewComponentView.mm
The boilerplate for Fabric component's implementation part will look like following:
#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:
//...
#import "RNNativeListViewContainerView.h"
#import "RNNativeListViewViewController.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:
//...
@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.
To learn more about anonymous function in C++ check Lambda expressions section in C++ reference
The last thing left is to implement scrollToItem:
method:
//...
@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 "RNNativeListViewContainerView.h"
#import "RNNativeListViewViewController.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!