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()
}
}