0

I am working on adding buttons to UICollectionViewCell at runtime and having the button registered to the event according to its type, at runtime. The type of the button can be, for instance, utterance button and action button, and both should be registered to separate events.

The hierarchy of UI elements is like UICollectionView -> UICollectionViewCell -> StackView -> Multiple (UIView -> UIButton).

The problem is that I am unable to register the .touchUpInside events.

What I have tried yet is calling becomeFirstResponder() through button's instance, moving the event method inside the UIViewController and adding target in cellForItemAt method after having the view hierarchy set up, trying isUserInteractionEnabled with values true and false on UICollectionView, UICollectionViewCell, UIStackView, UIView and UIButton.

EDIT 1: So the code looks like the following:

class AssistantViewController: UIViewController {
    private var assistantManager: AssistantManager
    private var conversationSection: UICollectionView
    private let layout:UICollectionViewFlowLayout = UICollectionViewFlowLayout.init()

    private var multipleChoiceMessageCell = MultipleChoiceMessageCell()
    private var conversationCells: [BaseCollectionViewCell] {
        return [
            multipleChoiceMessageCell
        ]
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        construct()
        assistantManager.performGreetingUtterance()
    }

}

extension AssistantViewController: UICollectionViewDataSource {
    @objc func didTapUtteranceButton() {
        print ("elo I a m here")
    }

    func collectionView(_ collectionView: UICollectionView,
                        cellForItemAt indexPath: IndexPath) -> 
  UICollectionViewCell {
        let conversationItem = assistantManager.conversationItems[indexPath.row]
        let cellId = self.getCellIdentifier(for: conversationItem)
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId,
                                                      for: indexPath) as? ConversationItemCell

        if let conversationCell = cell {
            conversationCell.layoutIfNeeded()

            conversationCell.configure(with: conversationItem)

            switch conversationItem {
            case let .multipleChoiceMessage(chatMessage, multipleChoice): do {

                for item in multipleChoice.multipleChoice.items {
                    switch item {
                    case .utteranceButton(let utteranceButton): do {
                        utteranceButton.button.isUserInteractionEnabled = true
                        utteranceButton.button.addTarget(self,
                                                         action: #selector(didTapUtteranceButton),
                                                         for: .touchUpInside)
                        //utteranceButton.button.becomeFirstResponder()
                        print("added")
                        }
                    }
                }
                }
            default: print("Todo")
            }
        }
        return cell!
    }

class AssistantManager {
    public func parseAssistantSnippets(
        snippets: [Snippet]) -> ConversationCellModel {
        var interactableItems: [MessageContent] = []
        var utteredText:String = ""

        for snippet in snippets {
            switch snippet {
            case .text(let text): utteredText = text.displayText
            case .utteranceButton(let utteranceButton): do {
                let button = UtteranceButton(buttonTitle: utteranceButton.displayText,
                                               maxWidth: ConversationCell.elementMaxWidth,
                                               utteranceText: utteranceButton.utteranceText,
                                               onPress: performUserUtterance)
                let messageContent = MessageContent.utteranceButton(button)

                interactableItems.push(messageContent)
                }
            default: print("TODO")
            }
        }
        return  ConversationCellModel.init(message: utteredText,
                                           multipleChoice: interactableItems)
    }

    public func performGreetingUtterance() {
        self.addTypingIndicator()

        performUtteranceOperation(utterance: "hi")
    }

    public func performUtteranceOperation(utterance: String) {
        let context = UtteranceRequest.UserContext(cid: "1",
                                                   location: CLLocationCoordinate2D(),
                                                   storeID: "1",
                                                   timeZoneOffset: 0)
        let request = UtteranceRequest(utterance: utterance,
                                       context: context)

        provider.conveyUtterance(request) { [weak self] result in
            switch result {
            case .success(let snippetResponse): do {
                let snippetResponseTransformed = SnippetResponse.init(snippetResponse)
                let ConversationCellModel = self?.parseAssistantSnippets(
                    snippets: snippetResponseTransformed.snippets)

                if let model = ConversationCellModel {
                    self?.addMessageFromAssistant(interactableItems: model.multipleChoice,
                                                       utteredText: model.message)
                }
                }
            case .failure(let error): return
            }
        }
    }
}

class MultipleChoiceMessageCell: ConversationCell {
    public func configure(with conversationItem: ConversationItem) {
        switch conversationItem {
        case let .multipleChoiceMessage(chatMessage, multipleChoice): do {
            let isCellFromUser = chatMessage.metadata.isFromUser
            let avatarBackgroundColor = chatMessage.metadata.avatarBackgroundColor
            let avatarTintColor = chatMessage.metadata.avatarTintColor
            let messageTextBackgroundColor = chatMessage.metadata.textContainerBackgroundColor
            let messageTextColor = chatMessage.metadata.textColor

            self.messageTextLabel.text = chatMessage.message
            self.avatarImageView.image = chatMessage.metadata.avatarImage
            self.messageTextContainer.backgroundColor = messageTextBackgroundColor
            self.messageTextLabel.textColor = messageTextColor
            self.avatarImageView.backgroundColor = avatarBackgroundColor
            self.avatarImageView.tintColor = avatarTintColor
            self.rightMessageHorizontalConstraint?.isActive = isCellFromUser
            self.leftMessageHorizontalConstraint?.isActive = !isCellFromUser
            self.rightAvatarHorizontalConstraint?.isActive = isCellFromUser
            self.leftAvatarHorizontalConstraint?.isActive = !isCellFromUser

            configureCellWith(interactablesSection: multipleChoice.multipleChoice.itemsContainer)
        }
        default:
            logIncorrectMapping(expected: "message", actual: conversationItem)
        }
    }

    func configureCellWith(interactablesSection: UIStackView) {
        let stackViewTop = NSLayoutConstraint(
            item: interactablesSection,
            attribute: .top,
            relatedBy: .equal,
            toItem: self.messageTextContainer,
            attribute: .bottom,
            multiplier: 1,
            constant: 10)
        let stackViewLeading = NSLayoutConstraint(
            item: interactablesSection,
            attribute: .leading,
            relatedBy: .equal,
            toItem: self,
            attribute: .leading,
            multiplier: 1,
            constant: ConversationCell.avatarMaxSize +
                ConversationCell.messageContainerMaxLeadingMargin +
                ConversationCell.avatarMaxMargin)

        self.addAutoLayoutSubview(interactablesSection)

        self.addConstraints([stackViewTop,
                             stackViewLeading])
    }
}

class InteractableItem {
    let contentView: UIView
    var height: CGFloat

    init() {
        contentView = UIView()
        height = 0
    }
}

class CellPrimaryButton: InteractableItem {
    let button: CorePrimaryButton

    init(buttonTitle titleLabel: String, maxWidth: CGFloat) {
        button = CorePrimaryButton()
        button.setTitle(titleLabel, for: .normal)
        button.setBackgroundColor(CoreColor.white, for: .normal)
        button.setTitleColor(CoreColor.black, for: .normal)
        button.layer.borderColor = CoreColor.black.cgColor
        button.layer.borderWidth = 1
        //button.becomeFirstResponder()
        super.init()
        contentView.isUserInteractionEnabled = true
//        button.addTarget(self, action: #selector(didTapUtteranceButton), for: .touchUpInside)
        setConstraints(maxWidth: maxWidth)
    }

    func setConstraints(maxWidth: CGFloat) {
        let buttonHorizontal = NSLayoutConstraint(item: self.button,
                                                  attribute: .leading,
                                                  relatedBy: .equal,
                                                  toItem: self.contentView,
                                                  attribute: .leading,
                                                  multiplier: 1,
                                                  constant: 0)

        let buttonWidth = NSLayoutConstraint(item: self.button,
                                              attribute: .width,
                                              relatedBy: .lessThanOrEqual,
                                              toItem: nil,
                                              attribute: .notAnAttribute,
                                              multiplier: 1,
                                              constant: maxWidth)

        self.contentView.addConstraints([buttonHorizontal,
                                         buttonWidth])
        self.contentView.backgroundColor = UIColor.purple
        self.height = self.button.intrinsicContentSize.height
        self.contentView.addAutoLayoutSubview(self.button)
    }
}

class UtteranceButton: CellPrimaryButton {
    let utteranceText: String
    let performUtterance: (String) -> Void

    init(buttonTitle titleLabel: String,
         maxWidth: CGFloat,
         utteranceText: String,
         onPress: @escaping (String) -> Void) {

        self.utteranceText = utteranceText
        self.performUtterance = onPress

        super.init(buttonTitle: titleLabel, maxWidth: maxWidth)
    }
}

Fawad Khalil
  • 161
  • 1
  • 2
  • 11
  • Are the cells appearing correctly in the view? If you set `isUserInteractionEnabled ` to true, do you have visual feedback when you touch the `UIButton`? If you set it to false do you still have it? If you move the target to the `UICollectionViewCell` does it work? – André Jul 31 '19 at 01:22
  • @André Yes, cells are appearing correctly in the view. No, the `UIButton` does not provide visual feedback in either cases. No, it does not work if the target is moved to `UICollectionViewCell`. Also I have experimented with the `didSelectItemAt` method of `UICollectionViewDataSource` and it gets called every time. So my guess is that the cell is interfering with the input to the buttons, but how to disable this behaviour? – Fawad Khalil Jul 31 '19 at 08:59
  • @André Also, there is a speak button under `UIView` in the `UIViewController` and it works correctly. So there shouldn't be any problem with `UIViewController`. The problem is essentially with the children of `UICollectionView`. – Fawad Khalil Jul 31 '19 at 09:08
  • @André if I make `isUserInteractionEnabled` as `false`, then the `didSelectItemAt` does not get called. – Fawad Khalil Jul 31 '19 at 09:34
  • Right. I would suggest you create a simple `UICollectionViewCell` and inserting a simple `UIButton` inside it to see if it works. The insert one more element from your hierarchy, test and see if it still works. Do it until you find where is the problem. But I have to say, the way you're doing the things do not look traditional. – André Jul 31 '19 at 14:33
  • @André Ah, yes. I will give it a try. BTW I am open to suggestions, where do you feel that it should be done the other way? – Fawad Khalil Aug 01 '19 at 08:34

1 Answers1

0

Try this way of calling an action.

UIApplication.shared.sendAction(#selector(didTapUtteranceButton), to: self, from: nil, for: nil)

instead of this one:

utteranceButton.button.addTarget(self, #selector(didTapUtteranceButton), for: .touchUpInside)
Blazej SLEBODA
  • 8,936
  • 7
  • 53
  • 93
  • I came to a strange observation. I created another project with the same approach of adding button and events and it was working fine. May be it's some other issue in this specific project. I am working on some other projects for now, and will give it a try, as well, later. Thanks by the way :) – Fawad Khalil Sep 06 '19 at 13:51