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 RangeSliderPackage
inside. When it's expanded, it will show all files that we created under range-slider-package/ios
directory.
- ObjC++ & Swift
- ObjC++ only
Add native library as dependency for the package
When developing some native code, you might end up in a need to add some iOS library.
Each RN library that includes some iOS native code is integrated to your project thanks to CocoaPods.
And that's also the case for our module - we have RangeSliderPackage.podspec
, so let's navigate there and add a native dependency.
# `.podspec` file is like "`package.json`" for iOS CocoaPods packages
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
# Detect if new arch is enabled
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
Pod::Spec.new do |s|
s.name = "RangeSliderPackage"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "13.0" }
s.author = package["author"]
s.source = { :git => package["repository"], :tag => "#{s.version}" }
# This is crucial - declare which files will be included in the package (similar to "files" field in `package.json`)
s.source_files = "ios/**/*.{h,m,mm,swift}"
+ # Declare dependency (similar to entries under "dependencies" field in `package.json`)
+ s.dependency 'RangeUISlider', '~> 3.0'
if new_arch_enabled
s.pod_target_xcconfig = {
"DEFINES_MODULE" => "YES",
"SWIFT_OBJC_INTERFACE_HEADER_NAME" => "RangeSliderPackage-Swift.h",
# This is handy when we want to detect if new arch is enabled in Swift code
# and can be used like:
# #if RANGE_SLIDER_PACKAGE_NEW_ARCH_ENABLED
# // do sth when new arch is enabled
# #else
# // do sth when old arch is enabled
# #endif
"OTHER_SWIFT_FLAGS" => "-DRANGE_SLIDER_PACKAGE_NEW_ARCH_ENABLED"
}
else
s.pod_target_xcconfig = {
"DEFINES_MODULE" => "YES",
"SWIFT_OBJC_INTERFACE_HEADER_NAME" => "RangeSliderPackage-Swift.h"
}
end
# Install all React Native dependencies (RN >= 0.71 must be used)
#
# check source code for more context
# https://github.com/facebook/react-native/blob/0.71-stable/scripts/react_native_pods.rb#L172#L180
install_modules_dependencies(s)
end
For the range slider view we will use RangeUISlider
library.
To declare the dependency in the podspec, we add s.dependency "<name of the native library>"
- in this case it's s.dependency 'RangeUISlider', '~> 3.0'
.
'~> 3.0'
syntax in the podspec is kind of similar to declaring some JS dependency in package.json with version "^3"
- so the version needs to be at least 3.0.0
, but less than 4.0.0
After that, you can run npx pod-install
from your app's directory and library should be installed.
RangeSliderView.swift
We will start by defining the boilerplate for our custom view:
import UIKit
@objc(RangeSliderViewDelegate)
public protocol RangeSliderViewDelegate {
func sendOnRangeSliderViewBeginDragEvent()
func sendOnRangeSliderViewEndDragEvent(minValue: Double, maxValue: Double)
func sendOnRangeSliderViewValueChangeEvent(minValue: Double, maxValue: Double)
}
@objc(RangeSliderView)
public class RangeSliderView: UIView {
@objc public weak var delegate: RangeSliderViewDelegate? = nil
@objc public var onRangeSliderViewBeginDrag: ((NSDictionary?) -> Void)? = nil
@objc public var onRangeSliderViewEndDrag: ((NSDictionary?) -> Void)? = nil
@objc public var onRangeSliderViewValueChange: ((NSDictionary?) -> Void)? = nil
@objc public var activeColor: UIColor = UIColor.systemBlue
@objc public var inactiveColor: UIColor = UIColor.systemGray
@objc public var minValue: Double = 0.0
@objc public var maxValue: Double = 1.0
@objc public var leftKnobValue: Double = 0.0
@objc public var rightKnobValue: Double = 1.0
@objc public var step: Int = 0
}
We'll export 2 things to the Objective-C world - our view and its delegate protocol. For now all view's properties have only some default values, but we will add some code later to forward them to the slider.
To make Swift elements accessible to Objective-C world, we have to do 4 things:
- make class extending
NSObject
- mark class and its methods (at least those methods that are meant to be exposed) as public
- mark class with
@objc(exported-objc-name)
decorator - mark exposed methods with
@objc
decorator
Next step is to create the slider - will wrap the slider in the object class that will keep the slider and react to the slider's events.
Add the following snippet at the top of the file:
import RangeUISlider
import UIKit
class RangeUISliderObject {
public weak var delegate: RangeSliderViewDelegate? = nil
public var slider: RangeUISlider = {
let rangeSlider = RangeUISlider()
rangeSlider.barCorners = 5
rangeSlider.barHeight = 10
rangeSlider.leftKnobColor = UIColor.systemBlue
rangeSlider.leftKnobCorners = 10
rangeSlider.leftKnobHeight = 20
rangeSlider.leftKnobWidth = 20
rangeSlider.rightKnobColor = UIColor.systemBlue
rangeSlider.rightKnobCorners = 10
rangeSlider.rightKnobHeight = 20
rangeSlider.rightKnobWidth = 20
rangeSlider.showKnobsLabels = true
return rangeSlider
}()
private var isDragging: Bool = false
init() {
self.slider.delegate = self
}
}
extension RangeUISliderObject: RangeUISliderDelegate {
public func rangeIsChanging(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
self.delegate?.sendOnRangeSliderViewValueChangeEvent(minValue: minValueSelected, maxValue: maxValueSelected)
}
public func rangeChangeFinished(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
if !isDragging {
return
}
self.delegate?.sendOnRangeSliderViewEndDragEvent(minValue: minValueSelected, maxValue: maxValueSelected)
self.isDragging = false
}
public func rangeChangeStarted() {
self.isDragging = true
self.delegate?.sendOnRangeSliderViewBeginDragEvent()
}
}
So first we create RangeUISliderObject
class, which will hold our slider.
The slider property is created with some default configuration assigned to the slider.
This object class will also react to the events emitted by slider - we do it by extending the object with RangeUISliderDelegate
protocol and setting slider's delegate property to this object's instance.
And to make sure delegate methods are fired only when slider is dragged, we use isDragging
flag.
For more about delegates and Delegate Pattern, see Apple's docs.
So now, let's try to combine them.
// ...
@objc(RangeSliderView)
public class RangeSliderView: UIView {
// ...
private var sliderObject = RangeUISliderObject()
public override init(frame: CGRect) {
super.init(frame: frame)
self.sliderObject.delegate = self.delegate
self.addSubview(self.sliderObject.slider)
self.sliderObject.slider.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.sliderObject.slider.topAnchor.constraint(equalTo: self.topAnchor),
self.sliderObject.slider.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.sliderObject.slider.trailingAnchor.constraint(equalTo: self.trailingAnchor),
self.sliderObject.slider.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// ...
}
Here, we're initializing slider object and the slider is added as a subview.
The slider object is created in the view's init(frame:)
initializer, where we also add the slider as a subview.
The slider subview needs to match its parent size, we do it by constraining its anchors to parent anchors.
For more on layout constraints, visit Auto Layout anchors section in Apple's docs.
The only thing left is to forward all properties to the slider:
// ...
@objc(RangeSliderView)
public class RangeSliderView: UIView {
@objc public weak var delegate: RangeSliderViewDelegate? = nil {
didSet {
sliderObject.delegate = delegate
}
}
@objc public var onRangeSliderViewBeginDrag: ((NSDictionary?) -> Void)? = nil
@objc public var onRangeSliderViewEndDrag: ((NSDictionary?) -> Void)? = nil
@objc public var onRangeSliderViewValueChange: ((NSDictionary?) -> Void)? = nil
private var sliderObject = RangeUISliderObject()
public override init(frame: CGRect) {
super.init(frame: frame)
self.sliderObject.delegate = self.delegate
self.addSubview(self.sliderObject.slider)
self.sliderObject.slider.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.sliderObject.slider.topAnchor.constraint(equalTo: self.topAnchor),
self.sliderObject.slider.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.sliderObject.slider.trailingAnchor.constraint(equalTo: self.trailingAnchor),
self.sliderObject.slider.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc public var activeColor: UIColor = UIColor.systemBlue {
didSet {
sliderObject.slider.rangeSelectedColor = activeColor
}
}
@objc public var inactiveColor: UIColor = UIColor.systemGray {
didSet {
sliderObject.slider.rangeNotSelectedColor = inactiveColor
}
}
@objc public var minValue: Double = 0.0 {
didSet {
sliderObject.slider.scaleMinValue = minValue
}
}
@objc public var maxValue: Double = 1.0 {
didSet {
sliderObject.slider.scaleMaxValue = maxValue
}
}
@objc public var leftKnobValue: Double = 0.0 {
didSet {
sliderObject.slider.changeLeftKnob(value: leftKnobValue)
sliderObject.slider.defaultValueLeftKnob = leftKnobValue
}
}
@objc public var rightKnobValue: Double = 1.0 {
didSet {
sliderObject.slider.changeRightKnob(value: rightKnobValue)
sliderObject.slider.defaultValueRightKnob = rightKnobValue
}
}
@objc public var step: Int = 0 {
didSet {
sliderObject.slider.stepIncrement = CGFloat(step)
}
}
}
So here we forward all the props and the delegate to the slider instance thanks to Swift's didSet
property observer.
We also set the frame of the slider, so that it can be resized if slider's parent changes its bounds.
Visit Swift's docs to learn about didSet
property observer
Good, now let's use this view inside view manager and Fabric component view.
Complete RangeSliderView.swift
file
import RangeUISlider
import UIKit
class RangeUISliderObject {
public weak var delegate: RangeSliderViewDelegate? = nil
public var slider: RangeUISlider = {
let rangeSlider = RangeUISlider()
rangeSlider.barCorners = 5
rangeSlider.barHeight = 10
rangeSlider.leftKnobColor = UIColor.systemBlue
rangeSlider.leftKnobCorners = 10
rangeSlider.leftKnobHeight = 20
rangeSlider.leftKnobWidth = 20
rangeSlider.rightKnobColor = UIColor.systemBlue
rangeSlider.rightKnobCorners = 10
rangeSlider.rightKnobHeight = 20
rangeSlider.rightKnobWidth = 20
rangeSlider.showKnobsLabels = true
return rangeSlider
}()
private var isDragging: Bool = false
init() {
self.slider.delegate = self
}
}
extension RangeUISliderObject: RangeUISliderDelegate {
public func rangeIsChanging(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
self.delegate?.sendOnRangeSliderViewValueChangeEvent(minValue: minValueSelected, maxValue: maxValueSelected)
}
public func rangeChangeFinished(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
if !isDragging {
return
}
self.delegate?.sendOnRangeSliderViewEndDragEvent(minValue: minValueSelected, maxValue: maxValueSelected)
self.isDragging = false
}
public func rangeChangeStarted() {
self.isDragging = true
self.delegate?.sendOnRangeSliderViewBeginDragEvent()
}
}
@objc(RangeSliderViewDelegate)
public protocol RangeSliderViewDelegate {
func sendOnRangeSliderViewBeginDragEvent()
func sendOnRangeSliderViewEndDragEvent(minValue: Double, maxValue: Double)
func sendOnRangeSliderViewValueChangeEvent(minValue: Double, maxValue: Double)
}
@objc(RangeSliderView)
public class RangeSliderView: UIView {
@objc public weak var delegate: RangeSliderViewDelegate? = nil {
didSet {
sliderObject.delegate = delegate
}
}
@objc public var onRangeSliderViewBeginDrag: ((NSDictionary?) -> Void)? = nil
@objc public var onRangeSliderViewEndDrag: ((NSDictionary?) -> Void)? = nil
@objc public var onRangeSliderViewValueChange: ((NSDictionary?) -> Void)? = nil
private var sliderObject = RangeUISliderObject()
public override init(frame: CGRect) {
super.init(frame: frame)
self.sliderObject.delegate = self.delegate
self.addSubview(self.sliderObject.slider)
self.sliderObject.slider.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.sliderObject.slider.topAnchor.constraint(equalTo: self.topAnchor),
self.sliderObject.slider.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.sliderObject.slider.trailingAnchor.constraint(equalTo: self.trailingAnchor),
self.sliderObject.slider.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc public var activeColor: UIColor = UIColor.systemBlue {
didSet {
sliderObject.slider.rangeSelectedColor = activeColor
}
}
@objc public var inactiveColor: UIColor = UIColor.systemGray {
didSet {
sliderObject.slider.rangeNotSelectedColor = inactiveColor
}
}
@objc public var minValue: Double = 0.0 {
didSet {
sliderObject.slider.scaleMinValue = minValue
}
}
@objc public var maxValue: Double = 1.0 {
didSet {
sliderObject.slider.scaleMaxValue = maxValue
}
}
@objc public var leftKnobValue: Double = 0.0 {
didSet {
sliderObject.slider.changeLeftKnob(value: leftKnobValue)
sliderObject.slider.defaultValueLeftKnob = leftKnobValue
}
}
@objc public var rightKnobValue: Double = 1.0 {
didSet {
sliderObject.slider.changeRightKnob(value: rightKnobValue)
sliderObject.slider.defaultValueRightKnob = rightKnobValue
}
}
@objc public var step: Int = 0 {
didSet {
sliderObject.slider.stepIncrement = CGFloat(step)
}
}
}
RangeSliderViewManager.h
As usual the view manager header will be simple:
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>
@class RangeSliderView;
@interface RangeSliderViewManager : 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.
The RangeSliderView
Swift class has its "forward-declaration" (check out Apple's Swift-ObjC interop dedicated docs section).
RangeSliderViewManager.mm
#import "RangeSliderViewManager.h"
#import "RangeSliderPackage-Swift.h"
#if RCT_NEW_ARCH_ENABLED
#else
@interface RangeSliderViewManager () <RangeSliderViewDelegate>
@end
#endif
@implementation RangeSliderViewManager {
RangeSliderView *sliderView;
}
RCT_EXPORT_MODULE(RangeSliderView)
RCT_EXPORT_VIEW_PROPERTY(activeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(inactiveColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(leftKnobValue, double)
RCT_EXPORT_VIEW_PROPERTY(minValue, double)
RCT_EXPORT_VIEW_PROPERTY(maxValue, double)
RCT_EXPORT_VIEW_PROPERTY(rightKnobValue, double)
RCT_EXPORT_VIEW_PROPERTY(step, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(onRangeSliderViewBeginDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRangeSliderViewEndDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRangeSliderViewValueChange, RCTDirectEventBlock)
#if RCT_NEW_ARCH_ENABLED
#else
RCT_EXPORT_METHOD(setLeftKnobValueProgrammatically:(nonnull NSNumber*) reactTag value:(NSInteger) value) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[RangeSliderView class]]) {
return;
}
[(RangeSliderView *)view setLeftKnobValue:value];
}];
}
RCT_EXPORT_METHOD(setRightKnobValueProgrammatically:(nonnull NSNumber*) reactTag value:(NSInteger) value) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[RangeSliderView class]]) {
return;
}
[(RangeSliderView *) view setRightKnobValue:value];
}];
}
- (void)sendOnRangeSliderViewValueChangeEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (sliderView.onRangeSliderViewValueChange) {
sliderView.onRangeSliderViewValueChange(@{ @"leftKnobValue": @(minValue), @"rightKnobValue": @(maxValue) });
}
}
- (void)sendOnRangeSliderViewBeginDragEvent
{
if (sliderView.onRangeSliderViewBeginDrag) {
sliderView.onRangeSliderViewBeginDrag(nil);
}
}
- (void)sendOnRangeSliderViewEndDragEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (sliderView.onRangeSliderViewEndDrag) {
sliderView.onRangeSliderViewEndDrag(@{ @"leftKnobValue": @(minValue), @"rightKnobValue": @(maxValue) });
}
}
- (UIView *)view
{
RangeSliderView *view = [RangeSliderView new];
view.delegate = self;
sliderView = view;
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 do 2 additional things:
- declaring native commands (using
RCT_EXPORT_METHOD
macro andRCTUIManager
class) - handling events emitting (with
RCTDirectEventBlock
props andRangeSliderViewDelegate
methods)
The view
getter is also declared for the old arch - for new arch we are just using Fabric component view.
RangeSliderViewComponentView.h
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
@class RangeSliderView;
@interface RangeSliderViewComponentView : RCTViewComponentView
@end
#endif
Here the Fabric component view extends base RCTViewComponentView
class.
The RangeSliderView
Swift class has its "forward-declaration" (check out Apple's Swift-ObjC interop dedicated docs section).
RangeSliderViewComponentView.mm
The implementation for the Fabric component will be quite large, so let's try to break it into parts.
The first part - boilerplate:
#if RCT_NEW_ARCH_ENABLED
#import "RangeSliderViewComponentView.h"
#import <React/RCTConversions.h>
#import <react/renderer/components/RangeSliderPackage/ComponentDescriptors.h>
#import <react/renderer/components/RangeSliderPackage/EventEmitters.h>
#import <react/renderer/components/RangeSliderPackage/Props.h>
#import <react/renderer/components/RangeSliderPackage/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@interface RangeSliderViewComponentView () <RCTRangeSliderViewViewProtocol>
@end
@implementation RangeSliderViewComponentView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RangeSliderViewProps>();
_props = defaultProps;
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(props);
[super updateProps:props oldProps:oldProps];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// That component does not accept child views
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// That component does not accept child views
}
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRangeSliderViewHandleCommand(self, commandName, args);
}
- (void)setLeftKnobValueProgrammatically:(double)value
{
//
}
- (void)setRightKnobValueProgrammatically:(double)value
{
//
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RangeSliderViewComponentDescriptor>();
}
@end
Class<RCTComponentViewProtocol> RangeSliderViewCls(void)
{
return RangeSliderViewComponentView.class;
}
#endif
Component begins with some new arch imports and conversion helpers.
We make the component extending code-generated protocol - this protocol has all the native commands methods that we declared in JS spec.
Next we implement all required methods and we create RangeSliderViewCls
function.
If you take a closer look, we override to methods related to the child components (- mountChildComponentView:index:
and - unmountChildComponentView:index:
).
Those methods can be used to control how the child views should be added/removed in the Fabric component.
In our case, we prevent adding/removal to be sure that our slider view does not have any child views.
Another interesting thing takes place in - handleCommand:args:
method - it invokes RCTRangeSliderViewHandleCommand
function.
That function is code-generated as well as RCTRangeSliderViewViewProtocol
protocol and is used to forward native commands calls, to dedicated methods (in our case: - setLeftKnobValueProgrammatically
& - setRightKnobValueProgrammatically
).
OK - second part, let's start filling the boilerplate with our slider view:
// ...
#import "RangeSliderPackage-Swift.h"
using namespace facebook::react;
@interface RangeSliderViewComponentView () <RCTRangeSliderViewViewProtocol>
@end
@interface RangeSliderViewComponentView () <RangeSliderViewDelegate>
@end
@implementation RangeSliderViewComponentView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RangeSliderViewProps>();
_props = defaultProps;
RangeSliderView *view = [RangeSliderView new];
view.delegate = self;
self.contentView = (UIView *)view;
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(props);
RangeSliderView *view = (RangeSliderView *)self.contentView;
if (oldViewProps.activeColor != newViewProps.activeColor) {
[view setActiveColor:RCTUIColorFromSharedColor(newViewProps.activeColor)];
}
if (oldViewProps.inactiveColor != newViewProps.inactiveColor) {
[view setInactiveColor:RCTUIColorFromSharedColor(newViewProps.inactiveColor)];
}
if (oldViewProps.step != newViewProps.step) {
[view setStep:newViewProps.step];
}
if (oldViewProps.minValue != newViewProps.minValue) {
[view setMinValue:newViewProps.minValue];
}
if (oldViewProps.maxValue != newViewProps.maxValue) {
[view setMaxValue:newViewProps.maxValue];
}
if (oldViewProps.leftKnobValue != newViewProps.leftKnobValue) {
[view setLeftKnobValue:newViewProps.leftKnobValue];
}
if (oldViewProps.rightKnobValue != newViewProps.rightKnobValue) {
[view setRightKnobValue:newViewProps.rightKnobValue];
}
[super updateProps:props oldProps:oldProps];
}
// ...
@end
// ...
What's going on here?
RangeSliderView
is imported viaRangeSliderPackage-Swift.h
importRangeSliderViewDelegate
is implemented by the Fabric componentRangeSliderView
is initialized and set as acontentView
, its delegate is set to this Fabric component's instance- all props are handled in
- updateProps:oldProps:
But that's still only half of the things - XCode is probably warning you that RangeSliderViewDelegate
is not implemented - let's fix it!
// ...
@implementation RangeSliderViewComponentView
// ...
- (void)sendOnRangeSliderViewValueChangeEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewValueChange(
RangeSliderViewEventEmitter::OnRangeSliderViewValueChange{
.leftKnobValue = minValue,
.rightKnobValue = maxValue
});
}
}
- (void)sendOnRangeSliderViewBeginDragEvent
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewBeginDrag(
RangeSliderViewEventEmitter::OnRangeSliderViewBeginDrag{});
}
}
- (void)sendOnRangeSliderViewEndDragEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewEndDrag(
RangeSliderViewEventEmitter::OnRangeSliderViewEndDrag{
.leftKnobValue = minValue,
.rightKnobValue = maxValue
});
}
}
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRangeSliderViewHandleCommand(self, commandName, args);
}
- (void)setLeftKnobValueProgrammatically:(double)value
{
RangeSliderView *view = (RangeSliderView *)self.contentView;
[view setLeftKnobValue:value];
}
- (void)setRightKnobValueProgrammatically:(double)value
{
RangeSliderView *view = (RangeSliderView *)self.contentView;
[view setRightKnobValue:value];
}
// ...
@end
// ...
So now all warnings should be cleared - delegate methods are implemented and we also handled native commands.
Event emitters in new architecture mode are done in C++ and are code-generated
Complete RangeSliderViewComponentView.mm
file
#if RCT_NEW_ARCH_ENABLED
#import "RangeSliderViewComponentView.h"
#import <React/RCTConversions.h>
#import <react/renderer/components/RangeSliderPackage/ComponentDescriptors.h>
#import <react/renderer/components/RangeSliderPackage/EventEmitters.h>
#import <react/renderer/components/RangeSliderPackage/Props.h>
#import <react/renderer/components/RangeSliderPackage/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
#import "RangeSliderPackage-Swift.h"
using namespace facebook::react;
@interface RangeSliderViewComponentView () <RCTRangeSliderViewViewProtocol>
@end
@interface RangeSliderViewComponentView () <RangeSliderViewDelegate>
@end
@implementation RangeSliderViewComponentView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RangeSliderViewProps>();
_props = defaultProps;
RangeSliderView *view = [RangeSliderView new];
view.delegate = self;
self.contentView = (UIView *)view;
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(props);
RangeSliderView *view = (RangeSliderView *)self.contentView;
if (oldViewProps.activeColor != newViewProps.activeColor) {
[view setActiveColor:RCTUIColorFromSharedColor(newViewProps.activeColor)];
}
if (oldViewProps.inactiveColor != newViewProps.inactiveColor) {
[view setInactiveColor:RCTUIColorFromSharedColor(newViewProps.inactiveColor)];
}
if (oldViewProps.step != newViewProps.step) {
[view setStep:newViewProps.step];
}
if (oldViewProps.minValue != newViewProps.minValue) {
[view setMinValue:newViewProps.minValue];
}
if (oldViewProps.maxValue != newViewProps.maxValue) {
[view setMaxValue:newViewProps.maxValue];
}
if (oldViewProps.leftKnobValue != newViewProps.leftKnobValue) {
[view setLeftKnobValue:newViewProps.leftKnobValue];
}
if (oldViewProps.rightKnobValue != newViewProps.rightKnobValue) {
[view setRightKnobValue:newViewProps.rightKnobValue];
}
[super updateProps:props oldProps:oldProps];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// That component does not accept child views
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// That component does not accept child views
}
- (void)sendOnRangeSliderViewValueChangeEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewValueChange(
RangeSliderViewEventEmitter::OnRangeSliderViewValueChange{
.leftKnobValue = minValue,
.rightKnobValue = maxValue
});
}
}
- (void)sendOnRangeSliderViewBeginDragEvent
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewBeginDrag(
RangeSliderViewEventEmitter::OnRangeSliderViewBeginDrag{});
}
}
- (void)sendOnRangeSliderViewEndDragEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewEndDrag(
RangeSliderViewEventEmitter::OnRangeSliderViewEndDrag{
.leftKnobValue = minValue,
.rightKnobValue = maxValue
});
}
}
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRangeSliderViewHandleCommand(self, commandName, args);
}
- (void)setLeftKnobValueProgrammatically:(double)value
{
RangeSliderView *view = (RangeSliderView *)self.contentView;
[view setLeftKnobValue:value];
}
- (void)setRightKnobValueProgrammatically:(double)value
{
RangeSliderView *view = (RangeSliderView *)self.contentView;
[view setRightKnobValue:value];
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RangeSliderViewComponentDescriptor>();
}
@end
Class<RCTComponentViewProtocol> RangeSliderViewCls(void)
{
return RangeSliderViewComponentView.class;
}
#endif
Add native library as dependency for the package
When developing some native code, you might end up in a need to add some iOS library.
Each RN library that includes some iOS native code is integrated to your project thanks to CocoaPods.
And that's also the case for our module - we have RangeSliderPackage.podspec
, so let's navigate there and add a native dependency.
# `.podspec` file is like "`package.json`" for iOS CocoaPods packages
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
# Detect if new arch is enabled
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
Pod::Spec.new do |s|
s.name = "RangeSliderPackage"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "13.0" }
s.author = package["author"]
s.source = { :git => package["repository"], :tag => "#{s.version}" }
# This is crucial - declare which files will be included in the package (similar to "files" field in `package.json`)
s.source_files = "ios/**/*.{h,m,mm,swift}"
+ # Declare dependency (similar to entries under "dependencies" field in `package.json`)
+ s.dependency 'RangeUISlider', '~> 3.0'
if new_arch_enabled
s.pod_target_xcconfig = {
"DEFINES_MODULE" => "YES",
"SWIFT_OBJC_INTERFACE_HEADER_NAME" => "RangeSliderPackage-Swift.h",
# This is handy when we want to detect if new arch is enabled in Swift code
# and can be used like:
# #if RANGE_SLIDER_PACKAGE_NEW_ARCH_ENABLED
# // do sth when new arch is enabled
# #else
# // do sth when old arch is enabled
# #endif
"OTHER_SWIFT_FLAGS" => "-DRANGE_SLIDER_PACKAGE_NEW_ARCH_ENABLED"
}
else
s.pod_target_xcconfig = {
"DEFINES_MODULE" => "YES",
"SWIFT_OBJC_INTERFACE_HEADER_NAME" => "RangeSliderPackage-Swift.h"
}
end
# Install all React Native dependencies (RN >= 0.71 must be used)
#
# check source code for more context
# https://github.com/facebook/react-native/blob/0.71-stable/scripts/react_native_pods.rb#L172#L180
install_modules_dependencies(s)
end
For the range slider view we will use RangeUISlider
library.
To declare the dependency in the podspec, we add s.dependency "<name of the native library>"
- in this case it's s.dependency 'RangeUISlider', '~> 3.0'
.
'~> 3.0'
syntax in the podspec is kind of similar to declaring some JS dependency in package.json with version "^3"
- so the version needs to be at least 3.0.0
, but less than 4.0.0
After that, you can run npx pod-install
from your app's directory and library should be installed.
Exposing Swift only library to Objective-C
Sometimes you may have to use some Swift-only libraries and sooner or later, you'll have to use them in Objective-C code.
In our case, we need to somehow use our slider library in Objective-C view code. To do that we will create small Swift wrapper class that will expose functionality we need.
Let's start by creating our slider with some default configuration:
import RangeUISlider
import UIKit
class RangeUISliderObject {
public var slider: RangeUISlider = {
let rangeSlider = RangeUISlider()
rangeSlider.barCorners = 5
rangeSlider.barHeight = 10
rangeSlider.leftKnobColor = UIColor.systemBlue
rangeSlider.leftKnobCorners = 10
rangeSlider.leftKnobHeight = 20
rangeSlider.leftKnobWidth = 20
rangeSlider.rightKnobColor = UIColor.systemBlue
rangeSlider.rightKnobCorners = 10
rangeSlider.rightKnobHeight = 20
rangeSlider.rightKnobWidth = 20
rangeSlider.showKnobsLabels = true
return rangeSlider
}()
private var isDragging: Bool = false
init() {
self.slider.delegate = self
}
}
extension RangeUISliderObject: RangeUISliderDelegate {
public func rangeIsChanging(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
//
}
public func rangeChangeFinished(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
//
}
public func rangeChangeStarted() {
//
}
}
So first we create RangeUISliderObject
class, which will hold our slider.
The slider property is created with some default configuration assigned to the slider.
This object class will also react to the events emitted by slider - we do it by extending the object with RangeUISliderDelegate
protocol and setting slider's delegate property to this object's instance.
For now, slider event handlers are empty, but we will implement them in a second.
For more about delegates and Delegate Pattern, see Apple's docs.
Now it's the time to implement the Swift-Objective-C wrapper class - add the following code at the bottom of the file:
@objc(RangeUISliderWrapperDelegate)
public protocol RangeUISliderWrapperDelegate: AnyObject {
func sendOnRangeSliderViewBeginDragEvent()
func sendOnRangeSliderViewEndDragEvent(minValue: Double, maxValue: Double)
func sendOnRangeSliderViewValueChangeEvent(minValue: Double, maxValue: Double)
}
@objc(RangeUISliderWrapper)
public class RangeUISliderWrapper: NSObject {
@objc public weak var delegate: RangeUISliderWrapperDelegate? = nil
@objc public var activeColor: UIColor = UIColor.systemBlue
@objc public var inactiveColor: UIColor = UIColor.systemGray
@objc public var minValue: Double = 0.0
@objc public var maxValue: Double = 1.0
@objc public var leftKnobValue: Double = 0.0
@objc public var rightKnobValue: Double = 1.0
@objc public var step: Int = 0
}
In the Swift-Objective-C wrapper class we declare all the properties and methods that we want to expose to Objective-C code. Additionally, we declare delegate protocol that will be also available to implement in Objective-C receiver.
To make Swift elements accessible to Objective-C world, we have to do 4 things:
- make class extending
NSObject
- mark class and its methods (at least those methods that are meant to be exposed) as public
- mark class with
@objc(exported-objc-name)
decorator - mark exposed methods with
@objc
decorator
So now, let's try to combine them.
In RangeUISliderObject
class let's declare RangeUISliderWrapperDelegate
weak property, we will use to forward slider events to Objective-C receiver.
import RangeUISlider
import UIKit
class RangeUISliderObject {
public weak var delegate: RangeUISliderWrapperDelegate? = nil
// ...
}
Next, let's use that delegate property to properly implement RangeUISliderDelegate
:
import RangeUISlider
import UIKit
extension RangeUISliderObject: RangeUISliderDelegate {
public func rangeIsChanging(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
self.delegate?.sendOnRangeSliderViewValueChangeEvent(minValue: minValueSelected, maxValue: maxValueSelected)
}
public func rangeChangeFinished(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
if !isDragging {
return
}
self.delegate?.sendOnRangeSliderViewEndDragEvent(minValue: minValueSelected, maxValue: maxValueSelected)
self.isDragging = false
}
public func rangeChangeStarted() {
self.isDragging = true
self.delegate?.sendOnRangeSliderViewBeginDragEvent()
}
}
To make sure delegate methods are fired only when slider is dragged, we use isDragging
flag.
Now in RangeUISliderWrapper
we have to initialize instance of RangeUISliderObject
:
@objc(RangeUISliderWrapper)
public class RangeUISliderWrapper: NSObject {
// ...
private var sliderObject = RangeUISliderObject()
@objc public var slider: UIView {
get {
return sliderObject.slider
}
}
// ...
}
Cool! Finally we can wrap everything by forwarding events from slider and forwarding props to slider. Let's add following lines:
@objc(RangeUISliderWrapper)
public class RangeUISliderWrapper: NSObject {
@objc public weak var delegate: RangeUISliderWrapperDelegate? = nil {
didSet {
sliderObject.delegate = delegate
}
}
private var sliderObject = RangeUISliderObject()
@objc public var slider: UIView {
get {
return sliderObject.slider
}
}
@objc public var activeColor: UIColor = UIColor.systemBlue {
didSet {
sliderObject.slider.rangeSelectedColor = activeColor
}
}
@objc public var inactiveColor: UIColor = UIColor.systemGray {
didSet {
sliderObject.slider.rangeNotSelectedColor = inactiveColor
}
}
@objc public var minValue: Double = 0.0 {
didSet {
sliderObject.slider.scaleMinValue = minValue
}
}
@objc public var maxValue: Double = 1.0 {
didSet {
sliderObject.slider.scaleMaxValue = maxValue
}
}
@objc public var leftKnobValue: Double = 0.0 {
didSet {
sliderObject.slider.changeLeftKnob(value: leftKnobValue)
sliderObject.slider.defaultValueLeftKnob = leftKnobValue
}
}
@objc public var rightKnobValue: Double = 1.0 {
didSet {
sliderObject.slider.changeRightKnob(value: rightKnobValue)
sliderObject.slider.defaultValueRightKnob = rightKnobValue
}
}
@objc public var step: Int = 0 {
didSet {
sliderObject.slider.stepIncrement = CGFloat(step)
}
}
}
So here we forward all the props and the delegate to the slider instance thanks to Swift's didSet
property observer.
We also set the frame of the slider, so that it can be resized if slider's parent changes its bounds.
Visit Swift's docs to learn about didSet
property observer
Awesome! We have exposed Swift-only library and we can proceed with the rest of Objective-C code.
Complete RangeUISliderWrapper.swift
file
import RangeUISlider
import UIKit
class RangeUISliderObject {
public weak var delegate: RangeUISliderWrapperDelegate? = nil
public var slider: RangeUISlider = {
let rangeSlider = RangeUISlider()
rangeSlider.barCorners = 5
rangeSlider.barHeight = 10
rangeSlider.leftKnobColor = UIColor.systemBlue
rangeSlider.leftKnobCorners = 10
rangeSlider.leftKnobHeight = 20
rangeSlider.leftKnobWidth = 20
rangeSlider.rightKnobColor = UIColor.systemBlue
rangeSlider.rightKnobCorners = 10
rangeSlider.rightKnobHeight = 20
rangeSlider.rightKnobWidth = 20
rangeSlider.showKnobsLabels = true
return rangeSlider
}()
private var isDragging: Bool = false
init() {
self.slider.delegate = self
}
}
extension RangeUISliderObject: RangeUISliderDelegate {
public func rangeIsChanging(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
self.delegate?.sendOnRangeSliderViewValueChangeEvent(minValue: minValueSelected, maxValue: maxValueSelected)
}
public func rangeChangeFinished(minValueSelected: CGFloat, maxValueSelected: CGFloat, slider: RangeUISlider) {
if !isDragging {
return
}
self.delegate?.sendOnRangeSliderViewEndDragEvent(minValue: minValueSelected, maxValue: maxValueSelected)
self.isDragging = false
}
public func rangeChangeStarted() {
self.isDragging = true
self.delegate?.sendOnRangeSliderViewBeginDragEvent()
}
}
@objc(RangeUISliderWrapperDelegate)
public protocol RangeUISliderWrapperDelegate: AnyObject {
func sendOnRangeSliderViewBeginDragEvent()
func sendOnRangeSliderViewEndDragEvent(minValue: Double, maxValue: Double)
func sendOnRangeSliderViewValueChangeEvent(minValue: Double, maxValue: Double)
}
@objc(RangeUISliderWrapper)
public class RangeUISliderWrapper: NSObject {
@objc public weak var delegate: RangeUISliderWrapperDelegate? = nil {
didSet {
sliderObject.delegate = delegate
}
}
private var sliderObject = RangeUISliderObject()
@objc public var slider: UIView {
get {
return sliderObject.slider
}
}
@objc public var activeColor: UIColor = UIColor.systemBlue {
didSet {
sliderObject.slider.rangeSelectedColor = activeColor
}
}
@objc public var inactiveColor: UIColor = UIColor.systemGray {
didSet {
sliderObject.slider.rangeNotSelectedColor = inactiveColor
}
}
@objc public var minValue: Double = 0.0 {
didSet {
sliderObject.slider.scaleMinValue = minValue
}
}
@objc public var maxValue: Double = 1.0 {
didSet {
sliderObject.slider.scaleMaxValue = maxValue
}
}
@objc public var leftKnobValue: Double = 0.0 {
didSet {
sliderObject.slider.changeLeftKnob(value: leftKnobValue)
sliderObject.slider.defaultValueLeftKnob = leftKnobValue
}
}
@objc public var rightKnobValue: Double = 1.0 {
didSet {
sliderObject.slider.changeRightKnob(value: rightKnobValue)
sliderObject.slider.defaultValueRightKnob = rightKnobValue
}
}
@objc public var step: Int = 0 {
didSet {
sliderObject.slider.stepIncrement = CGFloat(step)
}
}
}
RangeSliderView.h
Now, let's work on the view that will hold our slider:
#import <UIKit/UIKit.h>
/**
* When using Swift classes in ObjC header, the class must have its
* "forward declaration"
*
* @see https://developer.apple.com/documentation/swift/importing-swift-into-objective-c#Include-Swift-Classes-in-Objective-C-Headers-Using-Forward-Declarations
*/
@class RangeUISliderWrapper;
@protocol RangeUISliderWrapperDelegate;
@protocol RangeSliderViewDelegate
- (void)sendOnRangeSliderViewBeginDragEvent;
- (void)sendOnRangeSliderViewEndDragEventWithMinValue:(double)minValue
maxValue:(double)maxValue;
- (void)sendOnRangeSliderViewValueChangeEventWithMinValue:(double)minValue
maxValue:(double)maxValue;
@end
@interface RangeSliderView : UIView
@property (nonatomic, weak) id <RangeSliderViewDelegate> _Nullable delegate;
@property (nonatomic, strong) UIColor * _Nonnull activeColor;
@property (nonatomic, strong) UIColor * _Nonnull inactiveColor;
@property (nonatomic) double minValue;
@property (nonatomic) double maxValue;
@property (nonatomic) double leftKnobValue;
@property (nonatomic) double rightKnobValue;
@property (nonatomic) NSInteger step;
@property (nonatomic, copy) void (^ _Nullable onRangeSliderViewBeginDrag)(NSDictionary * _Nullable);
@property (nonatomic, copy) void (^ _Nullable onRangeSliderViewEndDrag)(NSDictionary * _Nullable);
@property (nonatomic, copy) void (^ _Nullable onRangeSliderViewValueChange)(NSDictionary * _Nullable);
@end
We declare our custom view that extends UIView
and have some properties.
The view will also have weak delegate property - the delegate protocol is declared above the view.
Additionally, we do "forward declaration" for Swift elements we created before (check out Apple's Swift-ObjC interop dedicated docs section).
RangeSliderView.mm
With the header file ready, let's go to implementation file:
#import "RangeSliderView.h"
#import "RangeSliderPackage-Swift.h"
@implementation RangeSliderView {
RangeUISliderWrapper *swiftWrapper;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
swiftWrapper = [RangeUISliderWrapper new];
[self addSubview:swiftWrapper.slider];
swiftWrapper.slider.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[swiftWrapper.slider.topAnchor constraintEqualToAnchor:self.topAnchor],
[swiftWrapper.slider.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[swiftWrapper.slider.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[swiftWrapper.slider.bottomAnchor constraintEqualToAnchor:self.bottomAnchor]
]];
}
return self;
}
- (void)setDelegate:(id<RangeSliderViewDelegate>)delegate
{
_delegate = delegate;
swiftWrapper.delegate = (id<RangeUISliderWrapperDelegate>) delegate;
}
- (void)setActiveColor:(UIColor *)activeColor
{
_activeColor = activeColor;
[swiftWrapper setActiveColor:activeColor];
}
- (void)setInactiveColor:(UIColor *)inactiveColor
{
_inactiveColor = inactiveColor;
[swiftWrapper setInactiveColor:inactiveColor];
}
- (void)setMinValue:(double)minValue
{
_minValue = minValue;
[swiftWrapper setMinValue:minValue];
}
- (void)setMaxValue:(double)maxValue
{
_maxValue = maxValue;
[swiftWrapper setMaxValue:maxValue];
}
- (void)setLeftKnobValue:(double)leftKnobValue
{
_leftKnobValue = leftKnobValue;
[swiftWrapper setLeftKnobValue:leftKnobValue];
}
- (void)setRightKnobValue:(double)rightKnobValue
{
_rightKnobValue = rightKnobValue;
[swiftWrapper setRightKnobValue:rightKnobValue];
}
- (void)setStep:(NSInteger)step
{
_step = step;
[swiftWrapper setStep:step];
}
@end
It may be a lot of code, but most of it is just about forwarding the properties to the swift wrapper class.
The swift wrapper is created in the view's - initWithFrame:
initializer, where we also add the slider as a subview.
The slider subview needs to match its parent size, we do it by constraining its anchors to parent anchors.
For more on layout constraints, visit Auto Layout anchors section in Apple's docs.
Good, now let's use this view inside view manager and Fabric component view.
RangeSliderViewManager.h
As usual the view manager header will be simple:
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>
@interface RangeSliderViewManager : 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.
RangeSliderViewManager.mm
#import "RangeSliderViewManager.h"
#import "RangeSliderView.h"
#if RCT_NEW_ARCH_ENABLED
#else
@interface RangeSliderViewManager () <RangeSliderViewDelegate>
@end
#endif
@implementation RangeSliderViewManager {
RangeSliderView *sliderView;
}
RCT_EXPORT_MODULE(RangeSliderView)
RCT_EXPORT_VIEW_PROPERTY(activeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(inactiveColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(leftKnobValue, double)
RCT_EXPORT_VIEW_PROPERTY(minValue, double)
RCT_EXPORT_VIEW_PROPERTY(maxValue, double)
RCT_EXPORT_VIEW_PROPERTY(rightKnobValue, double)
RCT_EXPORT_VIEW_PROPERTY(step, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(onRangeSliderViewBeginDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRangeSliderViewEndDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRangeSliderViewValueChange, RCTDirectEventBlock)
#if RCT_NEW_ARCH_ENABLED
#else
RCT_EXPORT_METHOD(setLeftKnobValueProgrammatically:(nonnull NSNumber*) reactTag value:(NSInteger) value) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[RangeSliderView class]]) {
return;
}
[(RangeSliderView *)view setLeftKnobValue:value];
}];
}
RCT_EXPORT_METHOD(setRightKnobValueProgrammatically:(nonnull NSNumber*) reactTag value:(NSInteger) value) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[RangeSliderView class]]) {
return;
}
[(RangeSliderView *) view setRightKnobValue:value];
}];
}
- (void)sendOnRangeSliderViewValueChangeEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (sliderView.onRangeSliderViewValueChange) {
sliderView.onRangeSliderViewValueChange(@{ @"leftKnobValue": @(minValue), @"rightKnobValue": @(maxValue) });
}
}
- (void)sendOnRangeSliderViewBeginDragEvent
{
if (sliderView.onRangeSliderViewBeginDrag) {
sliderView.onRangeSliderViewBeginDrag(nil);
}
}
- (void)sendOnRangeSliderViewEndDragEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (sliderView.onRangeSliderViewEndDrag) {
sliderView.onRangeSliderViewEndDrag(@{ @"leftKnobValue": @(minValue), @"rightKnobValue": @(maxValue) });
}
}
- (UIView *)view
{
RangeSliderView *view = [RangeSliderView new];
view.delegate = self;
sliderView = view;
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 do 2 additional things:
- declaring native commands (using
RCT_EXPORT_METHOD
macro andRCTUIManager
class) - handling events emitting (with
RCTDirectEventBlock
props andRangeSliderViewDelegate
methods)
The view
getter is also declared for the old arch - for new arch we are just using Fabric component view.
RangeSliderViewComponentView.h
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
@interface RangeSliderViewComponentView : RCTViewComponentView
@end
#endif
Nothing fancy here, Fabric component view extending base RCTViewComponentView
class.
RangeSliderViewComponentView.mm
The implementation for the Fabric component will be quite large, so let's try to break it into parts.
The first part - boilerplate:
#if RCT_NEW_ARCH_ENABLED
#import "RangeSliderViewComponentView.h"
#import <React/RCTConversions.h>
#import <react/renderer/components/RangeSliderPackage/ComponentDescriptors.h>
#import <react/renderer/components/RangeSliderPackage/EventEmitters.h>
#import <react/renderer/components/RangeSliderPackage/Props.h>
#import <react/renderer/components/RangeSliderPackage/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@interface RangeSliderViewComponentView () <RCTRangeSliderViewViewProtocol>
@end
@implementation RangeSliderViewComponentView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RangeSliderViewProps>();
_props = defaultProps;
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(props);
[super updateProps:props oldProps:oldProps];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// That component does not accept child views
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// That component does not accept child views
}
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRangeSliderViewHandleCommand(self, commandName, args);
}
- (void)setLeftKnobValueProgrammatically:(double)value
{
//
}
- (void)setRightKnobValueProgrammatically:(double)value
{
//
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RangeSliderViewComponentDescriptor>();
}
@end
Class<RCTComponentViewProtocol> RangeSliderViewCls(void)
{
return RangeSliderViewComponentView.class;
}
#endif
Component begins with some new arch imports and conversion helpers.
We make the component extending code-generated protocol - this protocol has all the native commands methods that we declared in JS spec.
Next we implement all required methods and we create RangeSliderViewCls
function.
If you take a closer look, we override two methods related to the child components (- mountChildComponentView:index:
and - unmountChildComponentView:index:
).
Those methods can be used to control how the child views should be added/removed in the Fabric component.
In our case, we prevent adding/removal to be sure that our slider view does not have any child views.
Another interesting thing takes place in - handleCommand:args:
method - it invokes RCTRangeSliderViewHandleCommand
function.
That function is code-generated as well as RCTRangeSliderViewViewProtocol
protocol and is used to forward native commands calls, to dedicated methods (in our case: - setLeftKnobValueProgrammatically
& - setRightKnobValueProgrammatically
).
OK - second part, let's start filling the boilerplate with our slider view:
// ...
#import "RangeSliderView.h"
using namespace facebook::react;
@interface RangeSliderViewComponentView () <RCTRangeSliderViewViewProtocol>
@end
@interface RangeSliderViewComponentView () <RangeSliderViewDelegate>
@end
@implementation RangeSliderViewComponentView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RangeSliderViewProps>();
_props = defaultProps;
RangeSliderView *view = [RangeSliderView new];
view.delegate = self;
self.contentView = (UIView *)view;
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(props);
RangeSliderView *view = (RangeSliderView *)self.contentView;
if (oldViewProps.activeColor != newViewProps.activeColor) {
[view setActiveColor:RCTUIColorFromSharedColor(newViewProps.activeColor)];
}
if (oldViewProps.inactiveColor != newViewProps.inactiveColor) {
[view setInactiveColor:RCTUIColorFromSharedColor(newViewProps.inactiveColor)];
}
if (oldViewProps.step != newViewProps.step) {
[view setStep:newViewProps.step];
}
if (oldViewProps.minValue != newViewProps.minValue) {
[view setMinValue:newViewProps.minValue];
}
if (oldViewProps.maxValue != newViewProps.maxValue) {
[view setMaxValue:newViewProps.maxValue];
}
if (oldViewProps.leftKnobValue != newViewProps.leftKnobValue) {
[view setLeftKnobValue:newViewProps.leftKnobValue];
}
if (oldViewProps.rightKnobValue != newViewProps.rightKnobValue) {
[view setRightKnobValue:newViewProps.rightKnobValue];
}
[super updateProps:props oldProps:oldProps];
}
// ...
@end
// ...
What's going on here?
RangeSliderView
is importedRangeSliderViewDelegate
is implemented by the Fabric componentRangeSliderView
is initialized and set as acontentView
, its delegate is set to this Fabric component's instance- all props are handled in
- updateProps:oldProps:
But that's still only half of the things - XCode is probably warning you that RangeSliderViewDelegate
is not implemented - let's fix it!
// ...
@implementation RangeSliderViewComponentView
// ...
- (void)sendOnRangeSliderViewValueChangeEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewValueChange(
RangeSliderViewEventEmitter::OnRangeSliderViewValueChange{
.leftKnobValue = minValue,
.rightKnobValue = maxValue
});
}
}
- (void)sendOnRangeSliderViewBeginDragEvent
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewBeginDrag(
RangeSliderViewEventEmitter::OnRangeSliderViewBeginDrag{});
}
}
- (void)sendOnRangeSliderViewEndDragEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewEndDrag(
RangeSliderViewEventEmitter::OnRangeSliderViewEndDrag{
.leftKnobValue = minValue,
.rightKnobValue = maxValue
});
}
}
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRangeSliderViewHandleCommand(self, commandName, args);
}
- (void)setLeftKnobValueProgrammatically:(double)value
{
RangeSliderView *view = (RangeSliderView *)self.contentView;
[view setLeftKnobValue:value];
}
- (void)setRightKnobValueProgrammatically:(double)value
{
RangeSliderView *view = (RangeSliderView *)self.contentView;
[view setRightKnobValue:value];
}
// ...
@end
// ...
So now all warnings should be cleared - delegate methods are implemented and we also handled native commands.
Event emitters in new architecture mode are done in C++ and are code-generated
Complete RangeSliderViewComponentView.mm
file
#if RCT_NEW_ARCH_ENABLED
#import "RangeSliderViewComponentView.h"
#import <React/RCTConversions.h>
#import <react/renderer/components/RangeSliderPackage/ComponentDescriptors.h>
#import <react/renderer/components/RangeSliderPackage/EventEmitters.h>
#import <react/renderer/components/RangeSliderPackage/Props.h>
#import <react/renderer/components/RangeSliderPackage/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
#import "RangeSliderView.h"
using namespace facebook::react;
@interface RangeSliderViewComponentView () <RCTRangeSliderViewViewProtocol>
@end
@interface RangeSliderViewComponentView () <RangeSliderViewDelegate>
@end
@implementation RangeSliderViewComponentView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RangeSliderViewProps>();
_props = defaultProps;
RangeSliderView *view = [RangeSliderView new];
view.delegate = self;
self.contentView = (UIView *)view;
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(_props);
const auto &newViewProps = *std::static_pointer_cast<const RangeSliderViewProps>(props);
RangeSliderView *view = (RangeSliderView *)self.contentView;
if (oldViewProps.activeColor != newViewProps.activeColor) {
[view setActiveColor:RCTUIColorFromSharedColor(newViewProps.activeColor)];
}
if (oldViewProps.inactiveColor != newViewProps.inactiveColor) {
[view setInactiveColor:RCTUIColorFromSharedColor(newViewProps.inactiveColor)];
}
if (oldViewProps.step != newViewProps.step) {
[view setStep:newViewProps.step];
}
if (oldViewProps.minValue != newViewProps.minValue) {
[view setMinValue:newViewProps.minValue];
}
if (oldViewProps.maxValue != newViewProps.maxValue) {
[view setMaxValue:newViewProps.maxValue];
}
if (oldViewProps.leftKnobValue != newViewProps.leftKnobValue) {
[view setLeftKnobValue:newViewProps.leftKnobValue];
}
if (oldViewProps.rightKnobValue != newViewProps.rightKnobValue) {
[view setRightKnobValue:newViewProps.rightKnobValue];
}
[super updateProps:props oldProps:oldProps];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// That component does not accept child views
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// That component does not accept child views
}
- (void)sendOnRangeSliderViewValueChangeEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewValueChange(
RangeSliderViewEventEmitter::OnRangeSliderViewValueChange{
.leftKnobValue = minValue,
.rightKnobValue = maxValue
});
}
}
- (void)sendOnRangeSliderViewBeginDragEvent
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewBeginDrag(
RangeSliderViewEventEmitter::OnRangeSliderViewBeginDrag{});
}
}
- (void)sendOnRangeSliderViewEndDragEventWithMinValue:(double)minValue maxValue:(double)maxValue
{
if (_eventEmitter != nil) {
std::dynamic_pointer_cast<const RangeSliderViewEventEmitter>(_eventEmitter)
->onRangeSliderViewEndDrag(
RangeSliderViewEventEmitter::OnRangeSliderViewEndDrag{
.leftKnobValue = minValue,
.rightKnobValue = maxValue
});
}
}
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRangeSliderViewHandleCommand(self, commandName, args);
}
- (void)setLeftKnobValueProgrammatically:(double)value
{
RangeSliderView *view = (RangeSliderView *)self.contentView;
[view setLeftKnobValue:value];
}
- (void)setRightKnobValueProgrammatically:(double)value
{
RangeSliderView *view = (RangeSliderView *)self.contentView;
[view setRightKnobValue:value];
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RangeSliderViewComponentDescriptor>();
}
@end
Class<RCTComponentViewProtocol> RangeSliderViewCls(void)
{
return RangeSliderViewComponentView.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!