# 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 [#ios]

    ## Implement a custom plugin [#implement-a-custom-plugin-1]

    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 [#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 [#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 [#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 [#api-reference-1]

    ### AgoraBaseWidget [#agorabasewidget]

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

    #### Members [#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 [#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 [#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 [#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 [#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 [#sendmessage]

    Send a message to the observer.

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

    **Parameters**

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

    #### onLoad [#onload]

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

    ```swift
    void onLoad()
    ```

    #### onMessageReceived [#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 [#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 [#onroominfoupdated]

    The Controller notifies the widget of room information updates.

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

    **Parameters**

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

    #### onWidgetRoomPropertiesUpdated [#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 [#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 [#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 [#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. |

    
  
      
  
      
  
