How to Open Alert from UIBarButtonItem Selector Function

Hello Everyone,

I'm a bit new to Swift and iOS programming in general so I was hoping to get some feedback/help.

I'm working on a Markdown Editor, however, I've run into an issue getting a hyperlink prompt working from a UIBarButtonItem. The idea is to have a UIBarButtonItem open an alert with two TextFields. One asking for the URL and the other asking for the display text. Once the "Confirm" button is clicked in the Alert. It will insert the formatted markdown text.

Currently, I have a CustomUITextView inheriting from UITextView. The CustomUITextView is referenced in a CustomTextViewRepresentable which inherits from UIViewRepresentable.

Since UITextView doesn't have self.present. I'm not entirely sure how to go about it.

I took a snipped of the relevant code from my app. The code is very much Work-In-Progress so beware lol.

CustomViewRepresentable.swift

import UIKit
import SwiftUI

struct CursorPosition {
    var start: Int
    var end: Int
}

class CustomCursorPosition {
    public static var cursorPosition = CursorPosition(start: 0, end: 0)
}

class CustomTextView: UITextView {
    @objc func hyperlinkButtonTapped() -> Void {
        
    }
}

fileprivate struct CustomTextViewRepresentable: UIViewRepresentable {
    typealias UIViewType = CustomTextView
        
    @Binding var text: String
    var onDone: (() -> Void)?
    
    func makeUIView(context: UIViewRepresentableContext<CustomTextViewRepresentable>) -> CustomTextView {
        let textView = CustomTextView()
        textView.delegate = context.coordinator
        
        textView.isEditable = true
        textView.font = UIFont.preferredFont(forTextStyle: .body)
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true
        textView.isScrollEnabled = true
        textView.backgroundColor = UIColor.clear
        
        let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
        
        let linkButton = UIBarButtonItem(image: UIImage(systemName: "link"), style: .plain, target: self, action: #selector(textView.hyperlinkButtonTapped))
        
        toolBar.setItems([
            linkButton
        ], animated: true)
        textView.inputAccessoryView = toolBar
        
        
        if nil != onDone {
            textView.returnKeyType = .done
        }
        
        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        return textView
    }
    
    
    func updateUIView(_ uiView: CustomTextView, context: UIViewRepresentableContext<CustomTextViewRepresentable>) {
        if uiView.text != self.text {
            uiView.text = self.text
        }
        if uiView.window != nil, !uiView.isFirstResponder {
            DispatchQueue.main.async {
                uiView.becomeFirstResponder()
            }
        }
        
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text, onDone: onDone)
    }
    
    final class Coordinator: NSObject, UITextViewDelegate {
        var text: Binding<String>
        var onDone: (() -> Void)?
        
        init(text: Binding<String>, onDone: (() -> Void)? = nil) {
            self.text = text
            self.onDone = onDone
        }
        
        private func textViewDidChange(_ uiView: CustomTextView) {
            text.wrappedValue = uiView.text
        }
        
        private func textView(_ textView: CustomTextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            if let onDone = self.onDone, text == "\n" {
                textView.resignFirstResponder()
                onDone()
                return false
            }
            return true
        }
        
        private func textViewDidChangeSelection(_ textView: CustomTextView) {
            if let range = textView.selectedTextRange {
                CustomCursorPosition.cursorPosition.start = textView.offset(from: textView.beginningOfDocument, to: range.start)
                CustomCursorPosition.cursorPosition.end = textView.offset(from: textView.beginningOfDocument, to: range.end)
            }
        }
    }
    
}

struct EditText: View {
    private var placeholder: String
    private var onCommit: (() -> Void)?
    
    @Binding private var text: String
    private var internalText: Binding<String> {
        Binding<String>(get: { self.text } ) {
            self.text = $0
            self.showingPlaceholder = $0.isEmpty
        }
    }
    
    @State private var showingPlaceholder = false
    
    init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
        self.placeholder = placeholder
        self.onCommit = onCommit
        self._text = text
        self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
    }
    
    
    var body: some View {
        if #available(iOS 16.0, *) {
            NavigationStack {
                CustomTextViewRepresentable(text: self.internalText, onDone: onCommit)
                    .background(placeholderView, alignment: .topLeading)
            }
        } else {
            NavigationView {
                CustomTextViewRepresentable(text: self.internalText, onDone: onCommit)
                    .background(placeholderView, alignment: .topLeading)
            }        }
    }
    
    var placeholderView: some View {
        Group {
            if showingPlaceholder {
                Text(placeholder).foregroundColor(.gray)
                    .padding(.leading, 4)
                    .padding(.top, 8)
            }
        }
    }
}

TestEditor.swift

import SwiftUI

struct TestEditor: View {
    @State private var isShowing = false
    @State private var bodyText = ""
    var body: some View {
        Button("Hello", action: {
            isShowing.toggle()
        })
        .sheet(isPresented: $isShowing) {
            ModalSheet(isShowing: $isShowing)
        }
    }
}

struct ModalSheet: View {
    @State private var bodyText = ""
    @Binding var isShowing: Bool
    
    var body: some View {
        NavigationView {
            VStack {
                EditText(text: $bodyText)
            }.toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button(action: {
                        isShowing = false
                    }, label: {
                        Text("Cancel")
                    })
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button(action: {
                        isShowing = false
                    }, label: {
                        Text("Send")
                    })
                }
            }
            .navigationTitle("Test Editor")
            .navigationBarTitleDisplayMode(.inline)
        }
        
    }
}

struct TestEditor_Previews: PreviewProvider {
    static var previews: some View {
        TestEditor()
    }
}

A selector is a way to select a method to be called – it cannot encode a keypath or anything else like that. The way you've setup your UIBarButtonItem is to ask it to call a method named textView.hyperlinkButtonTapped on self (which in this case is a CustomTextViewRepresentable, which is also a struct and so fundamentally won't work, since the target-action pattern is an Obj-C concept and requires a class).

But you have a UITextView subclass that your creating here – you want to call hyperlinkButtonTapped on that. Which means instead of passing self you should pass textView and in turn you should pass #selector(hyperlinkButtonTapped) as the selector.

How to Open Alert from UIBarButtonItem Selector Function
 
 
Q