1

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 ?

Tarun Tyagi
  • 9,364
  • 2
  • 17
  • 30
user16780334
  • 422
  • 5
  • 20
  • 3
    Deleting and re-asking the same question every few hours isn't going to get you an answer any faster. – Paulw11 Jun 30 '21 at 03:22
  • @Paulw11 I have done some research and I just want to know what is this specific view . How it makes it a repetitive question? If yes I will be careful from next time – user16780334 Jun 30 '21 at 03:29
  • It is better to refine your existing question than to delete and ask again – Paulw11 Jun 30 '21 at 05:32
  • 2
    The message composition interface should not be customized, and its not recommended. Do check the documentation : https://developer.apple.com/documentation/messageui/mfmessagecomposeviewcontroller – RTXGamer Jun 30 '21 at 12:25
  • Check out the ObjC solution and see if it works for you. https://stackoverflow.com/questions/4786999/disable-editing-on-mfmailcomposeviewcontroller – RTXGamer Jun 30 '21 at 12:29

1 Answers1

4

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/

Tarun Tyagi
  • 9,364
  • 2
  • 17
  • 30
  • I tried your solution for disabling recipients/telephone edit by using _setCanEditRecipients: but I can edit the recipients field still. Any idea where I am wrong? – user16780334 Jul 02 '21 at 09:30
  • 1
    There's no guarantee that these private APIs will do what we think they will do and for the ones that are working today may not work in future versions. I strongly recommend not using these hacks in a production app. If you are interested in exploring how you can disable editing recepients list, please consider asking a different question focusing on that part only (you can mention what you have tried so far in the new question). – Tarun Tyagi Jul 02 '21 at 09:45
  • yes I have seen posts which recommend not to use the private API's and I will surely not use these in production apps yes I will post another question regarding that. Waiting for your valuable answer on that – user16780334 Jul 02 '21 at 09:49
  • can you please help me in this link https://stackoverflow.com/questions/68403441/how-to-encrypt-decrypt-the-files-such-as-pdf-txt-while-saving-and-reading-fr – user16780334 Jul 16 '21 at 05:16
  • to stop editing recipients I've used below code with this solution self.present(messageVC, animated: true) { [weak messageVC] in guard let controller = messageVC else {return}; let cHeight = controller.view.bounds.height; let cWidth = controller.view.bounds.width; let view = UIView(frame: CGRect(x: 0, y: 70, width: cWidth, height: 70)); view.backgroundColor = UIColor.gray.withAlphaComponent(0.001); view.autoresizingMask = [.flexibleWidth] view.isUserInteractionEnabled = false; controller.view.addSubview(view) } – Gopal krishan Jan 02 '23 at 06:32