Skip to content

UITableView Based Extension for iOS Sample

Extension Tutorial has shown an example of how to create an extension in FormCell page. In the tutorial, it requires developer to configure Height of control upfront, otherwise, in iOS, the height of the tableView cell containing the extension control in FormCell Page is fixed as default value. It cannot automatically adapt height according to its accommodated extension control. In reality, sometimes, it is tricky for developer to figure out the height value of control implemented in extension in design time.

This guide is to demonstrate an implementation example, by modifying the extension control module from the example project in the tutorial, to achieve auto-adaption of the cell’s height according to accommodated extension control in FormCell or SectionedTable page in iOS. The solution from this implementation example, in general, is to build extension control's iOS native view with a handler implementing ExtensionTableViewCellHandler, which is responsible to put extension control into a UITableViewCell and to integrate the UITableViewCell into the UITableView of FormCell container or SectionedTable. In this way, the cell's height can be managed according to the contained extension control, by leveraging systemLayoutSizeFitting of UITableViewCell.

First, complete development in Extension Tutorial to create a sample extension app. But, please skip “set the height” in No.4 at Step 3.

And then replace the file: MDK_Slider/Extensions/MySliderModule/controls/MySliderPlugin/ios/MySlider.ts with the following

import { View } from "tns-core-modules/ui/core/view";

/*
  This is a way to keep iOS and Android implementation of your extension separate
  We will encapsulate the MySlider class definition inside a function called GetMySliderClass
  This is so that the class definition won't be executed when you load this javascript
  via require function.
  The class definition will only be executed when you execute GetMySliderClass
*/
export function GetMySliderClass() {
  /**
   * IMPLEMENT THE IOS VERSION OF YOUR PLUGIN HERE
   */
  class MySliderTableViewCellHandler extends NSObject implements ExtensionTableViewCellHandler {
    public static ObjCProtocols = [ExtensionTableViewCellHandler];

    private _viewOwner;
    private _extensionView = null;

    private _slider_text = null;
    private _slider_value = null;
    private _slider_minValue = null;
    private _slider_maxValue = null;

    public getReuseIdentifier(): string {
      return MDKExtensionTableViewCell.reuseIdentifier;
    }

    public registerCellTo(tableView: UITableView) {
      tableView.registerClassForCellReuseIdentifier(MDKExtensionTableViewCell.class(), MDKExtensionTableViewCell.reuseIdentifier);
    }

    public configureReusableCell(tableViewCell: UITableViewCell) {
      let cell = <MDKExtensionTableViewCell> tableViewCell;
      this._extensionView = cell.getExtensionView();

      if (this._viewOwner) {
        this._extensionView.setOwner(this._viewOwner);
      }

      this._extensionView.setText(this._slider_text);
      this._extensionView.setValue(this._slider_value);
      this._extensionView.setMinValue(this._slider_minValue);
      this._extensionView.setMaxValue(this._slider_maxValue);
    }

    public initOwner(owner: any) {
      this._viewOwner = owner;
    }

    public disposeOwner() {
      this._viewOwner = null;
      if (this._extensionView) {
        this._extensionView.clearOwner();
      }
    }

    public setText(newText: string) {
      this._slider_text = newText;
      if (this._extensionView != null) {
        this._extensionView.setText(newText);
      }
    }

    public setValue(newValue: number) {
      this._slider_value = newValue;
      if (this._extensionView != null) {
        this._extensionView.setValue(newValue);
      }
    }

    public setMinValue(newMin: number) {
      this._slider_minValue = newMin;
      if (this._extensionView != null) {
        this._extensionView.setMinValue(newMin);
      }
    }

    public setMaxValue(newMax: number) {
      this._slider_maxValue = newMax;
      if (this._extensionView != null) {
        this._extensionView.setMaxValue(newMax);
      }
    }
  }

  class MDKExtensionTableViewCell extends UITableViewCell {
    private _cellContentView;
    protected _extensionView;
    public static reuseIdentifier: string = "MDKExtensionTableViewCell";

    initWithStyleReuseIdentifier(style: UITableViewCellStyle, reuseIdentifier: string): this {
      const cell = <this>super.initWithStyleReuseIdentifier(style, reuseIdentifier);
      this.setup();
      return cell;
    }

    initWithCoder(coder: NSCoder): this {
      const cell = <this>super.initWithCoder(coder);
      this.setup();
      return cell;
    }

    public setup() {
      this._extensionView = MySliderView.new();
      this._cellContentView = this._extensionView.create(this.contentView);
      this.contentView.addSubview(this._cellContentView);
    }

    public getExtensionView(): MySliderView {
      return this._extensionView;
    }

    systemLayoutSizeFittingSizeWithHorizontalFittingPriorityVerticalFittingPriority(targetSize: CGSize, horizontalFittingPriority: number, verticalFittingPriority: number): CGSize {
      this.layoutIfNeeded();
      let size = this._cellContentView.systemLayoutSizeFittingSizeWithHorizontalFittingPriorityVerticalFittingPriority(this._cellContentView.bounds.size, UILayoutPriorityRequired, UILayoutPriorityFittingSizeLevel);
      let calcSize = CGSizeMake(targetSize.width, size.height);

      return calcSize;
    }
  }

  // This is a class that handles the native event callbacks
  class SliderHandler extends NSObject {

    //This handler function will be called whenever the slider's value is changed
    // i.e. whenever user drag the slider's handle
    public valueChanged(nativeSlider: UISlider, nativeEvent: _UIEvent) {
      nativeSlider.value = Math.round(nativeSlider.value);
      const owner: MySlider = (<any>nativeSlider).owner;
      if (owner) {
        owner.setValue(nativeSlider.value);
      }
    }

    //This handler function will be called when user let go of the handle
    // This is where we will trigger an event called "OnSliderValueChanged" to the MDK Extension Class
    public afterValueChanged(nativeSlider: UISlider, nativeEvent: _UIEvent) {
      nativeSlider.value = Math.round(nativeSlider.value);
      const owner: MySlider = (<any>nativeSlider).owner;
      if (owner) {
        owner.setValue(nativeSlider.value);
        var eventData = {
          eventName: "OnSliderValueChanged",
          object: owner,
          value: nativeSlider.value
        };
        owner.notify(eventData);
      }
    }

    public static ObjCExposedMethods = {
        "valueChanged": { returns: interop.types.void, params: [interop.types.id, interop.types.id] },
        "afterValueChanged": { returns: interop.types.void, params: [interop.types.id, interop.types.id] }
    };
  }
  const handler = SliderHandler.new();

  class MySliderView extends NSObject {
    private _label;
    private _slider;
    private _layout;
    private _labelText = "";

    /**
     * Creates new native controls.
     */
    public create(contentView: UIView): UIView {
      //Create the Stack view - this is the main view of this extension
      this._layout = UIStackView.new();

      //Configuring the paddings around the stack view
      this._layout.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
      this._layout.layoutMarginsRelativeArrangement = true;
      let inset = new NSDirectionalEdgeInsets();
      inset.top = 8; inset.leading = 16; inset.bottom = 8; inset.trailing = 16;
      this._layout.directionalLayoutMargins = inset;
      // Set the layout stacking to be vertical
      this._layout.axis =  UILayoutConstraintAxis.Vertical;

      this._layout.autoresizesSubviews = true;
      this._layout.frame = CGRectMake(0, 0, contentView.bounds.size.width, contentView.bounds.size.height);

      //Create the label view
      this._label = UILabel.new();
      this._label.text = " ";
      this._label.font = this._label.font.fontWithSize(35); //Set font size
      this._label.textColor = UIColor.colorWithRedGreenBlueAlpha(106/255,109/255,112/255,1.0); //Set text color

      //Create the slider control
      this._slider = UISlider.new();

      //Assign a handler for whenever value changed i.e. when user is dragging the slider handle
      this._slider.addTargetActionForControlEvents(handler, "valueChanged", UIControlEvents.ValueChanged);
      //Assign a handler for when user let go of the handle
      this._slider.addTargetActionForControlEvents(handler, "afterValueChanged", UIControlEvents.TouchUpInside | UIControlEvents.TouchUpOutside);

      //Add the label and slider to the stack view
      this._layout.addArrangedSubview(this._label);
      this._layout.addArrangedSubview(this._slider);
      this._layout.setCustomSpacingAfterView(4, this._label); //Set the bottom margin of label

      //return the stack view
      return this._layout;
    }

    public setOwner(owner: View): void {
      (<any>this._slider).owner = owner;
      (<any>this._layout).owner = owner;
    }

    public clearOwner(): void {
      (<any>this._slider).owner = null;
      (<any>this._layout).owner = null;
    }

    public setText(newText: string): void {
      if (newText != null && newText != undefined) {
        this._labelText = newText;
        this._label.text = newText;
      }
    }

    public setValue(newVal: number): void {
      if (newVal != null && newVal != undefined) {
        this._slider.value = newVal;
        this._label.text = this._labelText + "(" + newVal + ")";
      }
    }

    public setMinValue(newMin: number): void {
      if (newMin != null && newMin != undefined) {
        this._slider.minimumValue = newMin;
      }
    }

    public setMaxValue(newMax: number): void {
      if (newMax != null && newMax != undefined) {
        this._slider.maximumValue = newMax;
      }
    }
  }

  class MySlider extends View {
    private _handler;

    public constructor(context: any) {
      super();
      this.createNativeView();
    }

    /**
     * Creates new native controls.
     */
    public createNativeView(): Object {
      this._handler = <MySliderTableViewCellHandler> MySliderTableViewCellHandler.new();

      //store the native view
      this.setNativeView(this._handler);

      //return the stack view
      return this._handler;
    }

    /**
     * Initializes properties/listeners of the native view.
     */
    initNativeView(): void {
      this._handler.initOwner(this);
      super.initNativeView();
    }

    /**
     * Clean up references to the native view and resets nativeView to its original state.
     * If you have changed nativeView in some other way except through setNative callbacks
     * you have a chance here to revert it back to its original state
     * so that it could be reused later.
     */
    disposeNativeView(): void {
      this._handler.disposeOwner();
    }

    //Must return the native view of the control for MDK FormCell and Section Extension
    public getView(): any {
      return this._handler;
    }

    public setText(newText: string): void {
      this._handler.setText(newText);
    }

    public setValue(newVal: number): void {
      this._handler.setValue(newVal);
    }

    public setMinValue(newMin: number): void {
      this._handler.setMinValue(newMin);
    }

    public setMaxValue(newMax: number): void {
      this._handler.setMaxValue(newMax);
    }
  }

  return MySlider;
}

Last update: November 2, 2022