Search code examples
uiactivityviewcontrollerfacebook-messengerimessage

Issues with transparent PNG in UIActivityViewController for FB Messenger and iMessage


I have been using the following code to call the UIActivityViewController for sharing stickers via the various social media apps:

if let image = sticker.getUIImage(), let imgData = UIImagePNGRepresentation(image) {
    let activityVC = UIActivityViewController(activityItems: [imgData], applicationActivities: nil)
    activityVC.popoverPresentationController?.sourceView = gesture.view
    self.present(activityVC, animated: true, completion: nil)
} else {
    ErrorHandler.handleError(STICKER_IMAGE_NOT_FOUND, sticker)
}

This code has been working fine until the most recent update to FB messenger (version 98.0). Now it shows an error "Couldn't load content". FB messenger appears to prefer a URL like this:

if let item = sticker.getImageURL() {
    let activityVC = UIActivityViewController(activityItems: [item], applicationActivities: nil)
    activityVC.popoverPresentationController?.sourceView = gesture.view
    self.present(activityVC, animated: true, completion: nil)
} else {
    ErrorHandler.handleError(STICKER_IMAGE_NOT_FOUND, sticker)
}

This works fine with FB Messenger but iMessage displays the transparent PNG with a black background. I was looking at UIActivityViewControllerCompletionWithItemsHandler but the discussion states it runs after the activity, too late for what I need to do. I also tried creating a custom UIActivity returning UIActivityType.message for activityType but it was added on to the bottom of the controller rather than taking over the default.

Is there a way to intercept the selection of the item in UIActivityViewController so I can use the MFMessageComposeViewController and add the UIImagePNGRepresentation to the message and allow all the others to use the URL?

Is there a particular argument type that I can pass to UIActivityViewController that will correctly display transparent PNG with all the social apps?

TIA Mike


Solution

  • Circling back to close this up. I eventually switched from using the UIActivityViewController universally to a system that customizes what is done for each type of service. I use BDGShare by Bob de Graaf. I build a list of the services that the app supports, show a button for each and then a switch to jump to each type of share. Just in case someone's working on this, here's what I came up with:

    The types of sharing the app wants to support:

    public enum ShareServiceType:Int {
        case iMessage=0, facebook, twitter, instagram, whatsApp, facebookMessenger, email, more
    }
    

    A class to store information about the type of sharing service

    public class ShareTargetVO: NSObject
    {
        var icon:String!
        var label:String!
        var type:ShareServiceType
    
        init( serviceType:ShareServiceType, icon:String, label:String )
        {
            self.type = serviceType
            self.icon = icon
            self.label = label
        }
    }
    

    A function in my social networking helper to populate the list of available services:

    static func getShareTargetList(for sticker : ReeSticker) -> [ShareTargetVO] {
        var services:Array<ShareTargetVO> = []
    
        // Check to see which capabilities are present and add buttons
        if (BDGShare.shared().isAvailable(forServiceType: SLServiceTypeFacebook)) {
            services.append(ShareTargetVO(serviceType: ShareServiceType.facebook, icon: "Icon-Share-Facebook", label: "Facebook"))
        }
        // Checking for facebook service type because there's no availability check with FBSDK for messenger (could be rolled into the lib)
        if (BDGShare.shared().isAvailable(forServiceType: SLServiceTypeFacebook) && !sticker.type.doesContain("video")) {
            services.append(ShareTargetVO(serviceType: ShareServiceType.facebookMessenger, icon: "Icon-Share-Messenger", label: "Messenger"))
        }
        if (BDGShare.shared().isAvailable(forServiceType: SLServiceTypeTwitter)) {
            services.append(ShareTargetVO(serviceType: ShareServiceType.twitter, icon: "Icon-Share-Twitter", label: "Twitter"))
        }
        if (BDGShare.shared().canSendSMS()) {
            services.append(ShareTargetVO(serviceType: ShareServiceType.iMessage, icon: "Icon-Share-Messages", label: "Messages"))
        }
        if UIApplication.shared.canOpenURL(URL(string: "whatsapp://")! as URL) && !sticker.type.contains("video") {
            services.append(ShareTargetVO(serviceType: ShareServiceType.whatsApp, icon: "Icon-Share-Whatsapp", label: "What's App?"))
        }
        if UIApplication.shared.canOpenURL(URL(string: "instagram://app")! as URL) && !sticker.type.contains("video") {
            services.append(ShareTargetVO(serviceType: ShareServiceType.instagram, icon: "Icon-Share-Instagram", label: "Instagram"))
        }
        if (BDGShare.shared().canSendEmail()) {
            services.append(ShareTargetVO(serviceType: ShareServiceType.email, icon: "Icon-Share-Mail", label: "Email"))
        }
        services.append(ShareTargetVO(serviceType: ShareServiceType.more, icon: "Icon-Share-More", label: "More"))
    
        return services
    }
    

    A function in my view controller to populate a UICollectionView of buttons for sharing limited to those services that are returned from the list function:

    func layoutShareButtons() {
    
        let f = self.view.frame
        let btnWidth = f.width * 0.82
        let bannerWidth = btnWidth + 10
    
        mask = UIView(frame: CGRect(x: 0, y: 0, width: f.width, height: f.height))
        mask.backgroundColor = .black
        mask.alpha = 0.3
        self.view.addSubview(mask)
    
        buttonList = SocialHelper.getShareTargetList(for: self.sticker)
    
        let buttonGridLayout = UICollectionViewFlowLayout()
        buttonGridLayout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
        buttonGridLayout.itemSize = CGSize(width: 60, height: 60)
        buttonGridLayout.scrollDirection = .horizontal
    
        buttonListView = UICollectionView(frame: CGRect(x: (f.width - bannerWidth) / 2,
                                                        y: self.preview.frame.origin.y + self.preview.frame.height + 10,
                                                        width: bannerWidth,
                                                        height: 80),
                                          collectionViewLayout: buttonGridLayout)
        buttonListView.register(ShareButtonCell.self, forCellWithReuseIdentifier: "shareButtonCell")
        buttonListView.dataSource = self
        buttonListView.delegate = self
        self.view.addSubview(buttonListView)
    
        // cancel button for sharing view
        // Button (added last to ensure it's on top of the z-order)
        cancelButton = SimpleButton(frame: CGRect(x: (f.width - bannerWidth) / 2, y: self.buttonListView.frame.origin.y + self.buttonListView.frame.height + 10, width: bannerWidth, height: 52))
        cancelButton.backgroundColor = UIColor(netHex:0x202020)
        cancelButton.layoutComponent(0, label: "Cancel")
        cancelButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.cancelButtonPressed(_:))))
        self.view.addSubview(self.cancelButton)
    }
    

    UICollectionView didSelect handler (for better SoC depending on your app remove the "share" function to a separate class just for the share implementation, in this app the screen we're working with is specifically for share):

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.share(shareTarget: buttonList[indexPath.row])
    }
    

    Finally, a function call the correct share type:

    func share(shareTarget: ShareTargetVO) {
    
        // Params to submit to service
        self.shareUrl = self.sticker.getStickerURL()
    
        let textStr = "" // BDGShare supports a message passed in as well but we just send the sticker
    
        // we need the NSData either way (sticker / video)
        var ok = true
        // try? is fine here because result is tested below
        let urlData : Data? = try? Data(contentsOf: self.shareUrl as URL)
        ok = (urlData != nil)
        var img: UIImage? = nil
        // if it's an image type then get it
        if ok {
            if (self.shareUrl.pathExtension.contains("png")) || (self.shareUrl.pathExtension.contains("jpg")) {
                img = UIImage(data: urlData! as Data)
                ok = (img != nil)
            }
        }
    
        if !ok {
            let alertCtrl = UIAlertController(title: "Error sending", message: "There was an error gathering the information for sending this sticker.", preferredStyle: .alert)
            alertCtrl.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
            self.present(alertCtrl, animated: true, completion: nil)
            return
        }
    
        switch shareTarget.type
        {
        case .iMessage:
            BDGShare.shared().shareSMS(textStr, recipient: nil, image: img, data:urlData! as Data, imageName: "sendSticker.png", completion:  {(SharingResult) -> Void in
                // Handle share result...
                self.handleShareResult(shareTarget.type, shareResult: SharingResult)
            })
            break
    
        case .facebook:
            BDGShare.shared().shareFacebook("", urlStr: "", image: img, completion: {(SharingResult) -> Void in
                // Handle share result...
                self.handleShareResult(shareTarget.type, shareResult: SharingResult)
            })
            break
    
        case .twitter: 
        ... more code for handling each type of share
        }
    }
    

    So there are all the pieces I used to implement using BDGShare to get around the UIActivityViewController. BTW - the "More" option at the end of the list calls that UIActivityViewController so it's still available to the user if they so choose.

    HTH, Mike