# Embed a custom plugin (/en/realtime-media/flexible-classroom/build/customize-the-ui-and-plugins/embed-custom-plugin/ios)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

In online classroom applications, some customization is often necessary to meet the needs of a particular use case. Agora provides Widgets to help users develop plugins according to their specific needs and embed them into Flexible Classroom.

      
  
      
## iOS

## Implement a custom plugin

This section uses the countdown plugin as an example to introduce the basic steps of implementing a custom plugin through a widget and embedding the plugin in Flexible Classroom.

### Implement the widget

Refer to the following steps to implement a custom widget:

1. Import the `AgoraWidget` library:

   ```swift
   import AgoraWidget
   ```

2. Initialize the widget's interface and data in the `onWidgetDidLoad` method:

   ```swift
   public override func onWidgetDidLoad() {
       super.onWidgetDidLoad()
       initViews()
       initConstraints()
       updateRoomData()
       updateViewData()
       updateViewFrame()

       log(
         content: info.roomProperties?.jsonString() ?? "nil",
         extra: nil,
         type: .info)
   }
   ```

3. Monitor `onWidgetRoomPropertiesUpdated` and `onMessageReceived` methods to update data:

   ```swift
   public override func onWidgetRoomPropertiesUpdated(
                       _ properties: [String : Any],
                       cause: [String : Any]?,
                       keyPaths: [String]) {
       super.onWidgetRoomPropertiesUpdated(
         properties,
         cause: cause,
         keyPaths: keyPaths)
       updateRoomData()
       updateViewData()
       shouldStartTime()

       log(
         content: properties.jsonString() ?? "nil",
         extra: cause?.jsonString(),
         type: .info)
   }

   public override func onMessageReceived(_ message: String) {
       super.onMessageReceived(message)

       if let timestamp = message.toSyncTimestamp() {
           objectCreateTimestamp = timestamp
           initCurrentTimestamp()
           shouldStartTime()
       }

       log(content: message, type: .info)
   }
   ```

4. If you need to change the size of the widget, send a message using `sendMessage`; then the parent container that listens to the message updates the size of the widget:

   ```swift
   func updateViewFrame() {
       let size = ["width": countdownView.neededSize.width, "height": countdownView.neededSize.height]

       guard let message = ["size": size].jsonString() else {
           return
       }

       sendMessage(message)
   }
   ```

### Register Widget in Agora Classroom SDK

Add a countdown widget to `AgoraEduLaunchConfig.widgets` using `AgoraWidgetConfig`, and register the implemented plugin to the Agora Classroom SDK during `launch`:

```swift
let launchConfig = AgoraEduLaunchConfig(
  userName: userName,
  userUuid: userUuid,
  userRole: userRole,
  roomName: roomName,
  roomUuid: roomUuid,
  roomType: roomType,
  appId: appId,
  token: token,
  startTime: nil,
  duration: nil,
  region: region,
  mediaOptions: mediaOptions,
  userProperties: nil)

let widgetId = "countdownTimer"

launchConfig.widgets[widgetId] = AgoraWidgetConfig(with: AgoraCountdownTimerWidget.self, widgetId: widgetId)

AgoraClassroomSDK.launch(
  launchConfig,
  success: successBlock,
  failure: failureBlock)
```

### Using Widgets in the Classroom

Refer to the following steps to integrate the countdown plugin in the classroom:

1. Add an observer of the widget activity through `AgoraEduContext.widget`. Use the `addWidgetActivityObserver` method to listen for the callback of the activity using `AgoraClassToolsViewController`:

   ```swift
   override func viewDidLoad() {
       super.viewDidLoad()
       contextPool.room.registerRoomEventHandler(self)
       contextPool.widget.add(self)
   }
   ```

2. Listen to the activity's callback and create or destroy the widget:

   ```swift
   extension AgoraClassToolsViewController: AgoraWidgetActivityObserver {
       // Create a widget when an `onWidgetActive` callback is received
       func onWidgetActive(_ widgetId: String) {
           createWidget(widgetId)
       }

       // When the `onWidgetInactive` callback is received, destroy the widget
       func onWidgetInactive(_ widgetId: String) {
           releaseWidget(widgetId)
       }
   }
   ```

3. Create the Widget object of `AgoraCountdownTimer` and implement the communication between the widget and the local client:

   ```swift
   // Call the createWidget method to create the Widget object of AgoraCountdownTimer
   func createWidget(_ widgetId: String) {
       let widgetController = contextPool.widget

       guard widgetIdList.contains(widgetId),
           // Call getWidgetConfig to get AgoraWidgetConfig of AgoraCountdownTimer
           let config = widgetController.getWidgetConfig(widgetId) else {
               return
       }

       if let _ = getWidget(widgetId) {
           return
       }

       // Add AgoraClassToolsViewController as the observer of widget syncFrame through addObserverForWidgetSyncFrame of AgoraWidgetContext, and monitor the callback of syncFrame
       widgetController.addObserver(forWidgetSyncFrame: self, widgetId: widgetId)

       // Add AgoraClassToolsViewController as an observer of widget message through addWidgetMessageObserver of AgoraWidgetContext, and monitor the callback of message
       widgetController.add(self, widgetId: widgetId)

       let widget = widgetController.create(config)

       view.addSubview(widget.view)

       switch widgetId {
       case PollWidgetId:
         pollWidget = widget
       case CountdownTimerWidgetId:
         countdownTimerWidget = widget
       case PopupQuizWidgetId:
         popupQuizWidget = widget
       default:
         return
       }

       sendWidgetCurrentTimestamp(widgetId)
   }

   func sendWidgetCurrentTimestamp(_ widgetId: String) {
       let syncTimestamp = contextPool.monitor.getSyncTimestamp()
       let tsDic = ["syncTimestamp": syncTimestamp]

       if let string = tsDic.jsonString() {
           // Call the sendMessage method to send the current timestamp to the widget
           contextPool.widget.sendMessage(toWidget: widgetId, message: string)
       }
   }
   ```

4. Implement the `syncFrame` callback

   ```swift
   extension AgoraClassToolsViewController: AgoraWidgetSyncFrameObserver {
       // When syncFrame is updated, update the position and size of the widget
       func onWidgetSyncFrameUpdated(
           _ syncFrame: CGRect,
           widgetId: String,
           operatorUser: AgoraWidgetUserInfo?) {
               let size = getWidgetSize(widgetId)
               updateWidgetFrame(widgetId, size: size)
       }
   }
   ```

5. Implement a callback method for listening to messages.

   ```swift
   extension AgoraClassToolsViewController: AgoraWidgetMessageObserver {
       // When the message callback is received, AgoraClassToolsViewController acts as the parent container to update the size of the widget
       func onMessageReceived(_ message: String, widgetId: String) {
           if let size = parseSizeMessage(widgetId: widgetId, message: message) {
               updateWidgetFrame(widgetId, size: size)
           }
       }
   }
   ```

## API reference

### AgoraBaseWidget

`AgoraBaseWidget` is the base class of a Widget. Widgets that implement specific functions need to inherit from this class.

#### Members

| Parameter | Type              | Description                                                                        |
| :-------- | :---------------- | :--------------------------------------------------------------------------------- |
| `info`    | `AgoraWidgetInfo` | The basic information required by the widget is assigned by the widget controller. |
| `view`    | `View`            | Widget's container view.                                                           |

#### updateWidgetRoomProperties

Update the widget's room properties. The updated room properties pass to the Controller through the `onWidgetRoomPropertiesUpdated` callback.

```swift
void updateWidgetRoomProperties(
    Map<String: Any> properties,
    Map<String: Any> cause,
    Callback<Void> success,
    Callback<Error> failure)
```

**Parameters**

| Parameter    | Type               | Description                              |
| :----------- | :----------------- | :--------------------------------------- |
| `properties` | `Map<String: Any>` | Properties to update, supports keyPath.  |
| `cause`      | `Map<String: Any>` | The reason for the update, can be empty. |
| `success`    | `Callback<Void>`   | Request succeeded.                       |
| `failure`    | `Callback<Error>`  | The request failed, returning an error.  |

#### deleteWidgetRoomProperties

Delete the widget's room property. The deleted room property passes to the Controller through the `onWidgetRoomPropertiesDeleted` callback.

```swift
void deleteWidgetRoomProperties(
    Array<String> keyPaths,
    Map<String: Any> cause,
    Callback<Void> success,
    Callback<Error> failure)
```

**Parameters**

| Parameter  | Type               | Description                                              |
| :--------- | :----------------- | :------------------------------------------------------- |
| `keyPaths` | `Array<String>`    | The key array of properties to delete, supports keyPath. |
| `cause`    | `Map<String: Any>` | The reason for the deletion, can be empty.               |
| `success`  | `Callback<Void>`   | Request succeeded.                                       |
| `failure`  | `Callback<Error>`  | The request failed, returning an error.                  |

#### updateWidgetUserProperties

Update the widget's user properties. The updated room properties pass to the Controller through the `onWidgetUserPropertiesUpdated` callback.

```swift
void updateWidgetUserProperties(
    Map<String: Any> properties,
    Map<String: Any> cause,
    Callback<Void> success,
    Callback<Error> failure)
```

**Parameters**

| Parameter    | Type               | Description                                                                       |
| :----------- | :----------------- | :-------------------------------------------------------------------------------- |
| `properties` | `Map<String: Any>` | Properties to update, supports keyPath. Only need to pass in the value to update. |
| `cause`      | `Map<String: Any>` | The reason for the update, can be empty.                                          |
| `success`    | `Callback<Void>`   | Request succeeded.                                                                |
| `failure`    | `Callback<Error>`  | The request failed, returning an error.                                           |

#### deleteWidgetUserProperties

Delete the user properties of the widget. The deleted room property passes to the Controller through the `onWidgetUserPropertiesDeleted` callback.

```swift
void deleteWidgetUserProperties(
    Array<String> keyPaths,
    Map<String: Any> cause,
    Callback<Void> success,
    Callback<Error> failure)
```

**Parameters**

| Parameter  | Type               | Description                                              |
| :--------- | :----------------- | :------------------------------------------------------- |
| `keyPaths` | `Array<String>`    | The key array of properties to delete, supports keyPath. |
| `cause`    | `Map<String: Any>` | The reason for the update, can be empty.                 |
| `success`  | `Callback<Void>`   | Request succeeded.                                       |
| `failure`  | `Callback<Error>`  | The request failed, returning an error.                  |

#### sendMessage

Send a message to the observer.

```swift
void sendMessage(String message)
```

**Parameters**

| Parameter | Type     | Description      |
| :-------- | :------- | :--------------- |
| `message` | `String` | Message content. |

#### onLoad

The widget loading is complete; it is loaded inside the Controller.

```swift
void onLoad()
```

#### onMessageReceived

The Controller notifies the widget of received messages.

The `sendMessageToWidget` callback is fired after calling in the `WidgetContext` protocol.

```swift
void onMessageReceived(String message)
```

**Parameters**

| Parameter | Type     | Description      |
| :-------- | :------- | :--------------- |
| `message` | `String` | Message content. |

#### onLocalUserInfoUpdated

The Controller notifies the widget of local user information updates.

```swift
void onLocalUserInfoUpdated(AgoraWidgetUserInfo localUserInfo)
```

**Parameters**

| Parameter       | Type                  | Description             |
| :-------------- | :-------------------- | :---------------------- |
| `localUserInfo` | `AgoraWidgetUserInfo` | Local user information. |

#### onRoomInfoUpdated

The Controller notifies the widget of room information updates.

```swift
void onRoomInfoUpdated(AgoraWidgetRoomInfo roomInfo)
```

**Parameters**

| Parameter  | Type                  | Description       |
| :--------- | :-------------------- | :---------------- |
| `roomInfo` | `AgoraWidgetRoomInfo` | Room information. |

#### onWidgetRoomPropertiesUpdated

The Controller notifies the widget of room property updates.

```swift
void onWidgetRoomPropertiesUpdated(
    Map<String: Any> properties,
    Map<String: Any> cause,
    Array<String> keyPaths,
    AgoraWidgetUserInfo operatorUser)
```

**Parameters**

| Parameter      | Type                  | Description                                |
| :------------- | :-------------------- | :----------------------------------------- |
| `properties`   | `Map<String: Any>`    | Final complete properties.                 |
| `cause`        | `Map<String: Any>`    | Reason, can be empty.                      |
| `keyPaths`     | `Array<String>`       | Array of keys for properties that changed. |
| `operatorUser` | `AgoraWidgetUserInfo` | Operator, can be empty.                    |

#### onWidgetRoomPropertiesDeleted

Controller notifies the widget of room attribute deletion.

```swift
void onWidgetRoomPropertiesDeleted(
    Map<String: Any> properties,
    Map<String: Any> cause,
    Array<String> keyPaths,
    AgoraWidgetUserInfo operatorUser)
```

**Parameters**

| Parameter      | Type                  | Description                                 |
| :------------- | :-------------------- | :------------------------------------------ |
| `properties`   | `Map<String: Any>`    | Final complete properties.                  |
| `cause`        | `Map<String: Any>`    | Reason, can be empty.                       |
| `keyPaths`     | `Array<String>`       | Array of keys for properties to be deleted. |
| `operatorUser` | `AgoraWidgetUserInfo` | Operator, can be empty.                     |

#### onWidgetUserPropertiesUpdated

The Controller notifies the widget of user attribute updates.

```swift
void onWidgetUserPropertiesUpdated(
    Map<String: Any> properties,
    Map<String: Any> cause,
    Array<String> keyPaths,
    AgoraWidgetUserInfo operatorUser)
```

**Parameters**

| Parameter      | Type                  | Description                                |
| :------------- | :-------------------- | :----------------------------------------- |
| `properties`   | `Map<String: Any>`    | Final complete properties.                 |
| `cause`        | `Map<String: Any>`    | Reason, can be empty.                      |
| `keyPaths`     | `Array<String>`       | Array of keys for properties that changed. |
| `operatorUser` | `AgoraWidgetUserInfo` | Operator, can be empty.                    |

#### onWidgetUserPropertiesDeleted

The Controller notifies the widget of user attribute deletions.

```swift
void onWidgetUserPropertiesDeleted(
    Map<String: Any> properties,
    Map<String: Any> cause,
    Array<String> keys)
```

**Parameters**

| Parameter    | Type               | Description                             |
| :----------- | :----------------- | :-------------------------------------- |
| `properties` | `Map<String: Any>` | Final complete properties.              |
| `cause`      | `Map<String: Any>` | Reason, can be empty.                   |
| `keys`       | `Array<String>`    | Array of keys for properties to delete. |

    
  
      
  
      
  
