stevengharris/MarkupEditor

Toolbar on iPhone that is set to keyboard location but also to persist at the bottom / top when keyboard dismisses

Opened this issue · 3 comments

Is it possible to reconfigure the code regarding the toolbarLocation such that on the iPhone, it will stay at the bottom (this is my use case) and then when the keyboard appears, it will follow the inputAccessory of the keyboard.

The converse is true, when the keyboard disappears, it will return to the bottom of the screen.

Probably. I hadn't thought it was useful since the MarkupWKWebView needs focus for the toolbar to do anything to it, and whenever it gets focus, the keyboard appears. At least as it works now, as soon as the keyboard is dismissed, the selection goes away in the MarkupWKWebView, and so, presumably, makes any functionality of the toolbar useless. OTOH, maybe you have a custom toolbar that is still useful! A bit more detail on your use case would help me avoid going off in the wrong direction when looking into it.

IMG_5805
IMG_5804

RPReplay_Final1705297673.MP4

I have attached the the recording of my use case.

Basically the use of MarkupEditor is a subcomponent of a screen that I have, there are other fields which are UITextFields.

The first condition is when I am having the context of the MarkupEditor, the toolbar should be presented, as for the other UITextField, I do not need the toolbar. However I am considering the possibility to have the toolbar (minus the options to format the text) so that my user can continue to perform other actions, but just not perform the actions relating to formatting. I know this might be a deviation from the intent to keep MarkupEditor methods in the world of MarkupEditor framework, but this is how I am using it.

The second condition is that the toolbar should remain visible if no keyboard is presented. This is because the other options on the toolbar are relating to attaching some file to the screen. Rat

I realized early on that the way I will be using your library will be different from the example app you have provided, thus I have created a customized variation of the views to handle my use case. But just want to check with you on this as I noticed that the accessoryView within MarkupWKWebView has placement and constraint logic which will affect the positioning of the toolbar.

Right now, I am using two toolbars, one to be supplied to webView.inputAccessoryView and the other to be placed at the bottom of the webView, using the logic of the toolbarLocation == .bottom.

toolbar = ComposeMarkupToolbarUIView.inputAccessory(markupDelegate: markupDelegate, withTextEditorButton: false) // this is for the toolbars on the bottom of the screen
toolbar.translatesAutoresizingMaskIntoConstraints = false
toolbarHeightConstraint = NSLayoutConstraint(item: toolbar!, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: MarkupEditor.toolbarStyle.height())
addSubview(toolbar) // Is on top
NSLayoutConstraint.activate([
  webView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
  webView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
  webView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor),
  webView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor),
  toolbar.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
  toolbarHeightConstraint,
  toolbar.leftAnchor.constraint(equalTo: self.leftAnchor),
  toolbar.rightAnchor.constraint(equalTo: self.rightAnchor)
])
   // for the scenario that requires an override of inputAccessoryView
webView.inputAccessoryView = ComposeMarkupToolbarUIView.inputAccessory(markupDelegate: markupDelegate)

Thanks for considering this.

Wouldn't it be easier to create your own custom toolbar and manage it yourself (i.e., MarkupEditor.toolbarLocation = .none) and then re-use the individual toolbars that the MarkupToolbar itself is composed of (e.g., InsertToolbar, StyleToolbar, etc) as needed? This is kind of ugly but maybe it shows the kind of thing you're after? (I had to add a public InsertToolbar.init() for this to work.)

This example doesn't even deal directly with an inputAccessoryView, but SwiftUI layout leaves the CustomToolbar above the keyboard when it comes up. If you need to set the inputAccessoryView, you can use a modified version of MarkupToolbarUIView to get the UIView needed from the CustomToolbar SwiftUI you create and perhaps assign it after markupLoaded is called. I used withKeyboardButton so I could dismiss the keyboard, but it's a little weird with (and doesn't work with) the TextField.

struct CustomToolbarView: View {
    
    @State private var text: String = ""
    @State private var demoHtml: String = "<h1>Hello World</h1>"
    @FocusState private var textFocused: Bool
    
    var body: some View {
        TextField(text: $text, label: { Text("Enter Title") })
            .focused($textFocused)
            .border(.secondary)
        MarkupEditorView(html: $demoHtml)
            .border(.primary)
        CustomToolbar()
    }
    
    public init() {
        MarkupEditor.toolbarLocation = .none
    }
    
}

and a CustomToolbar ripped off from MarkupToolbar:

public struct CustomToolbar: View {
    
    public let toolbarStyle: ToolbarStyle
    private let withKeyboardButton: Bool
    @ObservedObject private var observedWebView = MarkupEditor.observedWebView
    @ObservedObject private var selectionState = MarkupEditor.selectionState
    private var contents: ToolbarContents
    
    public var body: some View {
        ZStack(alignment: .topLeading) {
            HStack {
                ScrollView(.horizontal) {
                    HStack {
                        if contents.insert {
                            InsertToolbar()
                        }
                        if contents.style {
                            if contents.insert { Divider() }
                            StyleToolbar()
                        }
                        if contents.format {
                            if contents.insert || contents.style { Divider() }
                            FormatToolbar()
                        }
                        Spacer()                // Push everything to the left
                    }
                    .environmentObject(toolbarStyle)
                    .padding(EdgeInsets(top: 2, leading: 8, bottom: 2, trailing: 8))
                    .disabled(observedWebView.selectedWebView == nil || !selectionState.valid)
                }
                .onTapGesture {}    // To make the buttons responsive inside of the ScrollView
                if withKeyboardButton {
                    Spacer()
                    Divider()
                    ToolbarImageButton(
                        systemName: "keyboard.chevron.compact.down",
                        action: {
                            _ = MarkupEditor.selectedWebView?.resignFirstResponder()
                        }
                    )
                    Spacer()
                }
            }
        }
        // Because the icons in toolbars are sized based on font, we need to limit their dynamicTypeSize
        // or they become illegible at very large sizes.
        .dynamicTypeSize(.small ... .xLarge)
        .frame(height: MarkupEditor.toolbarStyle.height())
        .zIndex(999)
    }
    
    public init(_ style: ToolbarStyle.Style? = nil, contents: ToolbarContents? = nil, markupDelegate: MarkupDelegate? = nil, withKeyboardButton: Bool = true) {
        let toolbarStyle = style == nil ? MarkupEditor.toolbarStyle : ToolbarStyle(style!)
        self.toolbarStyle = toolbarStyle
        let toolbarContents = contents == nil ? MarkupEditor.toolbarContents : contents!
        self.contents = toolbarContents
        self.withKeyboardButton = withKeyboardButton
    }
    
}