Share extension as a shared component

Introduction

Hello everyone from the mobile platform of the company “Tensor”! My name is Galina and in this article I want to share the history of the development of our Share Extension.

Over the past 3 years, the number of mobile applications released by us has grown significantly, and in the process of their development, the requirements for sharing functionality have also increased. Each business task requires different options, whether it’s sending photos to a dialog or uploading a document to disk. Not every our application supports this or that functionality, but it is not rational to write a separate implementation for a new product. Therefore, the share extension has become a separate module, configured by connected external dependencies.

Let’s refresh our memory

For those who have not delved deeply into ios development or have forgotten what a Share Extension is, a brief digression into the subject area:
Share extension – a convenient interface that allows the user to share content with other applications, for example, sending a document from one messenger to another.

You can read more details here

Target Configuration

All the main functionality of sharing is stored in a separate “ShareExtension” module, in fact it is the same regular framework as all the others, it connects to the application via CocoaPods.

For further work, we usually create an appropriate target and replace the automatically generated ShareViewController with a dummy file, which must be bound to the ios-extension, we don’t need the storyboard either.

We add information about the target and its dependencies to the Podfile, specify NSExtensionPrincipalClass = ShareViewController from the submodule in the info.plist of the extension (in the current case it is ShareExtension.ShareViewController).

  Replacing the automatically generated ShareViewController with a dummy file

Replacing the automatically generated ShareViewController with a dummy file

Setting NSExtensionPrincipalClass

Setting NSExtensionPrincipalClass

Interaction with Share Extension

Let’s get acquainted with the protocols and structures that our sharing module operates on.

Since VLSI applications are constructors made up of many separate modules, each of these modules can have its own context for sharing, in this case it is ShareModuleContext

/// Элемент меню share extension
public struct ShareExtensionAction {
    /// Заголовок
    public let title: String
    
    /// Идентификатор экшена
    public let type: ShareExtensionActionType
    
    /// Идентификатор контекста
    public let contextId: String
    
    /// :nodoc:
    public init(title: String, type: ShareExtensionActionType, contextId: String) {
        self.title = title
        self.type = type
        self.contextId = contextId
    }
}

/// Протокол для обработки нажатия на пункт shareextension
public protocol ShareExtensionActionType {}
/// Протокол для VC, в который можно что-то зашарить
public protocol ShareExtensionViewController: UIViewController {
    
    /// Данные, которые передают
    var shareData: ShareData? { get set }
    
    /// Протокол обратной связи
    var shareControllerDelegate: ShareControllerDelegate? { get set }
    
    /// Тип выбранной опции в меню шаринга
    var selectedAction: ShareExtensionActionType { get set }
}

/// Протокол обратной связи
public protocol ShareControllerDelegate: AnyObject {
    
    /// Метод будет вызван, когда VC свернут
    func onDismiss()
}

Mechanism for setting available options

The share extension implemented by us is configured based on several factors:

  • ShareExtensionConfig.plist – a properties file that is created at the application level and can control which of the available ShareExtensionConfigItems (enumeration of modules that implement sharing) we want to connect.

import SbisServiceAPI

/// Айтемы, которые ожидаем получить из ShareExtensionConfig.plist
public enum ShareExtensionConfigItem: String, CaseIterable {
    case communicator 
    case disk 
    case preview 
    case buffer
}

/// Получение настроек списка пунктов в меню ShareExtension
public class Configuration {
    
    /// Получить возможные опции при шаринге
    /// - Returns: массив опций
    static public func getList() -> [ShareExtensionConfigItem] {
        // Если нет листа с настройками - отдаём все пункты
        guard let path = Bundle.main.path(forResource: .appTag, ofType: .plist) else {
            return ShareExtensionConfigItem.allCases
        }

        var configList: [ShareExtensionConfigItem] = []
    
        if let list = NSArray(contentsOfFile: path) as? [String] {
            for item in list {
                if let configItem = ShareExtensionConfigItem(rawValue: item) {
                    configList.append(configItem)
                }
            }
        } else {
            assertionFailure("Ошибка настроек для ShareExtensionConfig.plist")
        }
        
        return configList
    }
}


private extension String {
    static let appTagName = "ShareExtensionConfig"
    static let appTag = "ShareExtensionConfig"
    static let plist = "plist"
}

Each context implements the ability to provide information about the available getActionList() options and the data types it supports getAcceptedTypes()

Life cycle

When the ShareViewController’s viewDidLoad() is called, the presenter starts configuring the array of available options based on the previously described mechanism.

// Запрос разрешенных на уровне приложения опций
let configList = Configuration.getList()

// Маппинг опций из получанного списка в массив ShareModuleContext’ов
let allContexts: [ShareModuleContext] = configList.compactMap {
    switch $0 {
    case .communicator:
        return communicatorShareContext
    case .disk:
        return diskFullShareContext
    case .preview:
        return previewerContext
    case .buffer:
        return bufferShareContext
    case .demo:
        return demoShareContext
    }
}

let extensionItemList = extensionList.flatMap { Array($0.attachments ?? [] )}

// Выбор опций, поддерживающих шаринг текущего типа файлов
for context in allContexts {
    let supportedTypes = context.getAcceptedTypes()
    let supportedItems: [ShareExtensionData] = extensionItemList.compactMap { item in
        if let type = supportedTypes.first(where: item.hasItemConformingToTypeIdentifier) {
            let data = ShareExtensionData(
                item: item,
                ofType: type,
                type: ExtensionAttachmentType(withUTType: type)
            )
            return data
        } else {
            return nil
        }
    }
    
    if !supportedItems.isEmpty {
        availableShareContexts.append(context)
        shareContextAttachmentData[context.getContextId()] = supportedItems
    }
}

Each of the contexts performs certain necessary checks for the availability of functionality before returning an array of options to getActionList().

After we know what options we can offer the user, we display the configured menu:

  1. send contact;

  2. sending a picture from the gallery;

  3. sending text by a user who does not have rights to the company’s disk;

  4. sending a document by a user who only has access to the channel module.

Adding a new option

To add a new option, a demo module was created: SbisDemoShareExt, let’s try to display the ability of share extension to expand on its example.

import SnapKit
import SbisNavigationAPI
import SbisServiceAPI
import SbisTheme

enum DemoSharePreviewAction: ShareExtensionActionType {
    case demo
}

final class DemoShareViewController: UIViewController, ShareExtensionViewController {
    
    var selectedAction: ShareExtensionActionType = DemoSharePreviewAction.demo
    var shareData: ShareData?
    
    weak var shareControllerDelegate: ShareControllerDelegate?
    
    private let imageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationController?.isNavigationBarHidden = true

        configureUI()
        showData()
    }
    
    private func configureUI() {
        imageView.contentMode = .scaleAspectFit
        view.backgroundColor = colors.bonusSameBackgroundColor
        
        view.addSubview(imageView)
        imageView.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.centerY.equalToSuperview()
            $0.width.lessThanOrEqualToSuperview()
            $0.height.lessThanOrEqualToSuperview()
        }
    }
    
    private func showData() {
        guard let current = shareData?.getLocalFileList()?.first else {
            return
        }
        
        if case .localFile(let file) = current,
           let url = file.url {
            DispatchQueue.global().async {
                let image = UIImage(url: url)
                
                DispatchQueue.main.async { [weak self] in
                    self?.imageView.image = image
                }
            }
        }
    }
}

// получение доступа к теме приложения
extension DemoShareViewController: ThemedViewController { }
import MobileCoreServices
import SbisNavigationAPI

/// Контекст для shareExtension
public class DemoShareContext: ShareModuleContext {
    // модуль будет обрабатывать только картинки
    private static let acceptedTypes = [kUTTypeImage as String]
    /// Тип выбранной опции в меню шаринга
    public var selectedAction: ShareExtensionActionType?
    
    private let vc = DemoShareViewController()
    private let contextID = String(describing: DemoShareContext.self)

    /// Получение VC окна для логики share
    public func getShareViewController() -> ShareExtensionViewController {
        return vc
    }
    
    /// Получение типов данных, которые модуль обрабатывает
    public func getAcceptedTypes() -> [String] {
        return DemoShareContext.acceptedTypes
    }
    
    /// Получение ИД контекста
    public func getContextId() -> String {
        return contextID
    }
    
     /// Получение списка действий, которые умеет производить модуль
    public func getActionList() -> [ShareExtensionAction] {
       return [
        ShareExtensionAction(
            title: localization[.title],
            type: DemoSharePreviewAction.demo,
            contextId: contextID)
       ]
    }
}

Next, we pass dependencies to ShareExtension through DI, we have this DITranquility

In the SbisDemoShareExt module:

import DITranquillity
import SbisServiceAPI
import SbisNavigationAPI

/// for share
public final class DemoDIShareFramework: DIFramework {

    /// :nodoc:
    public static func load(container: DIContainer) {
        container.append(part: DemoDISharePart.self)
    }
}

private final class DemoDISharePart: DIPart {
    static func load(container: DIContainer) {
        container.register(DemoShareContext.init)
            .as(check: ShareModuleContext.self,
                name: ModuleName.demoShowcase.name()) { $0 }
    }
}

Basically ShareDIFramwork:

container.append(framework: DemoDIShareFramework.self)

We then need to add a new case to the ShareExtensionConfigItem, DemoShareContext to the Share Extension’s presenter, and to switch contexts when filtering.

We get a new option when sharing pictures:

Data exchange with the main application:

In conclusion, I would like to pay attention to the ways of exchanging information with the main application, we use several of them:

  • shared local container:
    Allows you to store a large amount of data in the form of files.
    In VLSI applications, the data processing is handled by the C++ controller layer, we also tell it the path to this directory when the application/extension starts.

FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId)
  • userDefaults:
    Great for storing app settings or user preferences (such as color theme), as well as other local information such as app install date or CookiesStorage.

UserDefaults(suiteName: groupId).object(forKey: key)

In addition to the methods we use, data exchange via keychain is possible, as well as the use of a common CoreData, one way or another, any method is tied to the AppGroup.

Unexpected Limitations

I would also like to note that the Share Extension has its own characteristics in terms of memory limits (120mb), the duration of processes, and more. We will not delve into this, but we will recommend you an article with all the details.

Conclusion

Despite the fact that you can easily find hundreds of guides on creating a Share Extension on the Internet, you need to understand that most of these instructions are relevant only for the most simple and trivial tasks.

Creating something more complex, filled with non-linear business logic, will bring you many interesting hours of debugging and searching for answers on the Internet. I hope that this article will be one of those answers or inspire someone to rework the existing implementation 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *