iosswiftiphonexcodemfmessagecomposeviewcontroller

MFMessageComposeViewController disable editing text entry?


Hello StackOverflow friends,

I am integrating a MFMessageViewController and I want to disable the editing area of it either by disallowing the keyboard to appear or by disabling the user interaction to it. Currently my code is :

import UIKit
import MessageUI

class ViewController: UIViewController,MFMessageComposeViewControllerDelegate,UITextFieldDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }



  @IBAction func sendSmsClick(_ sender: AnyObject) {
            guard MFMessageComposeViewController.canSendText() else {
                return
            }

            let messageVC = MFMessageComposeViewController()
            UIButton.appearance(whenContainedInInstancesOf: [MFMessageComposeViewController.self]).isUserInteractionEnabled = false
            messageVC.body = "Enter a message hjhjhjkhjkhjhjhjjhgjhghjgjhghjghjghjghjgjhghjghjgjhghjghjghghjghjghjghghjghjhjghjghjghhvvvbnvhvhghghguyguygyugugigiugiouiopuopuoppuuo";
            messageVC.recipients = ["Enter tel-nr"]
            messageVC.messageComposeDelegate = self;
            NSLog("Subviews %@", messageVC.view.subviews);
           // self.view.endEditing(true)
        
        self.present(messageVC, animated: false) {
           // self.getAllSubviews(view: messageVC.view)
            messageVC.view.loopViewHierarchy { (view, stop) in
                if view is UIButton {
                    /// use the view
                    print("here")
                    stop = true
                }
            }
           
        }
        
        
        
        }

        func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
            switch (result.rawValue) {
                case MessageComposeResult.cancelled.rawValue:
                print("Message was cancelled")
                self.dismiss(animated: true, completion: nil)
            case MessageComposeResult.failed.rawValue:
                print("Message failed")
                self.dismiss(animated: true, completion: nil)
            case MessageComposeResult.sent.rawValue:
                print("Message was sent")
                self.dismiss(animated: true, completion: nil)
            default:
                break;
            }
        }

It is working fine and I just want to get to know the specific UIElement which is above the keyboard and I want to disable it for for further editing. How can I achieve this ?


Solution

  • You could try printing the view hierarchy and find your text field from the subviews, the problem is - all of this UI is running into a different environment than your app and you won't find anything useful there.

    /// Helper for printing view hierarchy recursively
    extension UIView {
        func printViewHierarchy() {
            print(self)
            for view in self.subviews {
                view.printViewHierarchy()
            }
        }
    }
    
    self.present(messageVC, animated: true, completion: {
        // Try printing the view hierarchy after it has been loaded on to screen
        messageVC.view.printViewHierarchy()
    })
    
    // Here's what's printed by above
    <UILayoutContainerView: 0x100a04990; frame = (0 0; 375 627); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x28100ef10>; layer = <CALayer: 0x281eac720>>
    <UINavigationTransitionView: 0x100c0e0d0; frame = (0 0; 375 627); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x281eee860>>
    <UIViewControllerWrapperView: 0x100c0f990; frame = (0 0; 375 627); autoresize = W+H; layer = <CALayer: 0x281e975e0>>
    <UIView: 0x103b04900; frame = (0 0; 375 627); autoresize = W+H; layer = <CALayer: 0x281e8aa80>>
    <_UISizeTrackingView: 0x103a05f80; frame = (0 0; 375 627); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x281e84c60>>
    <_UIRemoteView: 0x103a077f0; frame = (0 0; 375 667); userInteractionEnabled = NO; layer = <CALayerHost: 0x281e84b20>>
    

    This _UIRemoteView will get in your way and you won't be able to find your target textField/button in the view hierarchy.


    What else can we do?

    Use private apis that will most likely get us a rejection from App Store.


    How to find what private apis can we use?

    ObjectiveC.runtime provides you ways to inspect class details (public + private).

    import ObjectiveC.runtime
    
    func printClassDetails(_ targetClass: AnyClass) {
        var varCount: UInt32 = 0
        let iVars = class_copyIvarList(targetClass, &varCount)
    
        var index = 0
        if let iVars = iVars {
            while index < varCount-1 {
                let iVar = iVars[index]
                if let name = ivar_getName(iVar) {
                    print("iVar ------------> \(String(cString: name))")
                }
                index += 1
            }
        }
        free(iVars)
    
        index = 0
        let methods = class_copyMethodList(targetClass, &varCount)
        if let methods = methods {
            while index < varCount-1 {
                let method = methods[index]
                let selector = method_getName(method)
                print("method ------------> \(NSStringFromSelector(selector))")
                index += 1
            }
        }
        free(methods)
    }
    

    With above code, if you try to inspect MFMessageComposeViewController class, you will see following.

    printClassDetails(MFMessageComposeViewController.self)
    
    iVar ------------> _internal
    iVar ------------> _messageComposeDelegate
    iVar ------------> _recipients
    iVar ------------> _body
    iVar ------------> _subject
    iVar ------------> _message
    iVar ------------> _currentAttachedVideoCount
    iVar ------------> _currentAttachedAudioCount
    iVar ------------> _currentAttachedImageCount
    iVar ------------> _UTITypes
    iVar ------------> _photoIDs
    iVar ------------> _cloudPhotoIDs
    iVar ------------> _contentText
    iVar ------------> _contentURLs
    iVar ------------> _chatGUID
    iVar ------------> _groupName
    iVar ------------> _shareSheetSessionID
    
    method ------------> recipients
    method ------------> subject
    method ------------> setGroupName:
    method ------------> setRecipients:
    method ------------> smsComposeControllerShouldSendMessageWithText:toRecipients:completion:
    method ------------> attachments
    method ------------> setMessageComposeDelegate:
    method ------------> addRichLinkData:withWebpageURL:
    method ------------> addAttachmentURL:withAlternateFilename:
    method ------------> addAttachmentData:typeIdentifier:filename:
    method ------------> setShareSheetSessionID:
    method ------------> automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers
    method ------------> message
    method ------------> body
    method ------------> setChatGUID:
    method ------------> setMessage:
    method ------------> chatGUID
    method ------------> viewWillDisappear:
    method ------------> contentText
    method ------------> setSubject:
    method ------------> viewDidLoad
    method ------------> attachmentURLs
    method ------------> groupName
    method ------------> setUTITypes:
    method ------------> dealloc
    method ------------> viewDidAppear:
    method ------------> viewWillAppear:
    method ------------> UTITypes
    method ------------> setContentText:
    method ------------> setBody:
    method ------------> smsComposeControllerCancelled:
    method ------------> smsComposeControllerSendStarted:
    method ------------> smsComposeControllerEntryViewContentInserted:
    method ------------> .cxx_destruct
    method ------------> photoIDs
    method ------------> setModalPresentationStyle:
    method ------------> setPhotoIDs:
    method ------------> setContentURLs:
    method ------------> setCloudPhotoIDs:
    method ------------> initWithNibName:bundle:
    method ------------> cloudPhotoIDs
    method ------------> contentURLs
    method ------------> shareSheetSessionID
    method ------------> disableUserAttachments
    method ------------> setCurrentAttachedVideoCount:
    method ------------> setCurrentAttachedAudioCount:
    method ------------> setCurrentAttachedImageCount:
    method ------------> _MIMETypeForURL:
    method ------------> _isVideoMIMEType:
    method ------------> _isAudioMIMEType:
    method ------------> _isImageMIMEType:
    method ------------> _contentTypeForMIMEType:
    method ------------> _updateAttachmentCountForAttachmentURL:
    method ------------> canAddAttachmentURL:
    method ------------> mutableAttachmentURLs
    method ------------> addAttachmentData:withAlternateFilename:
    method ------------> insertSharedItemAndReturnEntryViewFrame:withAlternateFilename:completion:
    method ------------> showSharedItemInEntryView
    method ------------> _setCanEditRecipients:
    method ------------> _setShouldDisableEntryField:
    method ------------> messageComposeDelegate
    method ------------> currentAttachedVideoCount
    method ------------> currentAttachedAudioCount
    

    _setShouldDisableEntryField looks interesting. How to use this?

    let messageVC = MFMessageComposeViewController()
    messageVC.body = "Enter a message"
    messageVC.recipients = ["Test Telephone #"]
    
    let name = [":","d","l","e","i","F","y","r","t","n","E","e","l","b","a","s","i","D","d","l","u","o","h","S","t","e","s","_"].reversed().joined()
    let sel = NSSelectorFromString(name)
    if messageVC.responds(to: sel) {
        messageVC.perform(sel, with: true)
    }
    
    self.present(messageVC, animated: true, completion: nil)
    

    Does it work?

    As of Xcode 12.5 & iOS 14.6 - it does.


    Why all the dance to get to the selector name?

    Maybe (and that's a BIG MAYBE) it will help avoid a rejection like following.

    We identified one or more issues with a recent delivery for your app. Please correct the following issues, then upload again.

    ITMS-90338: Non-public API usage - The app contains or inherits from non-public classes in Project: XXXXXXXXXXXXXX . If method names in your source code match the private Apple APIs listed above, altering your method names will help prevent this app from being flagged in future submissions. In addition, note that one or more of the above APIs may be located in a static library that was included with your app. If so, they must be removed. For further information, visit the Technical Support Information at http://developer.apple.com/support/technical/