Modifying keyboard toolbar / accessory view with WKWebView - ios

I'm using a WKWebView with a content-editable div as the core of a rich-text editor, and would like to modify the top toolbar above the keyboard—the one that contains the autocorrect suggestions and formatting buttons. (Not sure if this counts as an input accessory view or not).
I've found a few posts showing how to remove the bar, but none of them seems to work, and ideally I'd like to keep the autocorrect part anyway.
At least one app, Ulysses, does this (though I don't know if it's with a web view):
And indeed, I'm pretty sure I can achieve it by doing surgery on the keyboard view hierarchy...but that seems like a tedious and brittle approach.
Is there a better way?
Thanks!

Maybe this tutorial on UITextInputAssistantItem would be helpful.
That said, after fiddling around with this for a while, using WKWebView I still could only get this to work for the first time the keyboard displayed, and every time after that it would return to its original state. The only thing I found that consistently worked was something like the following:
class ViewController: UIViewController {
#IBOutlet weak private var webView: WKWebView!
private var contentView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
webView.loadHTMLString("<html><body><div contenteditable='true'></div></body></html>", baseURL: nil)
for subview in webView.scrollView.subviews {
if subview.classForCoder.description() == "WKContentView" {
contentView = subview
}
}
inputAssistantItem.leadingBarButtonGroups = [UIBarButtonItemGroup(barButtonItems: [UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed(_:)))], representativeItem: nil)]
inputAssistantItem.trailingBarButtonGroups = [UIBarButtonItemGroup(barButtonItems: [UIBarButtonItem(barButtonSystemItem: .camera, target: self, action: #selector(openCamera(_:))), UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(actionPressed(_:)))], representativeItem: nil)]
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: .UIKeyboardDidShow, object: nil)
}
#objc private func donePressed(_ sender: UIBarButtonItem) {
view.endEditing(true)
}
#objc private func openCamera(_ sender: UIBarButtonItem) {
print("camera pressed")
}
#objc private func actionPressed(_ sender: UIBarButtonItem) {
print("action pressed")
}
#objc private func keyboardDidShow() {
contentView?.inputAssistantItem.leadingBarButtonGroups = [UIBarButtonItemGroup(barButtonItems: [UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed(_:)))], representativeItem: nil)]
contentView?.inputAssistantItem.trailingBarButtonGroups = [UIBarButtonItemGroup(barButtonItems: [UIBarButtonItem(barButtonSystemItem: .camera, target: self, action: #selector(openCamera(_:))), UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(actionPressed(_:)))], representativeItem: nil)]
}
}
It will give something like this:
It's a bit frustrating to have to set this on the content view every time the keyboard shows, as well as on the view controller itself, and I hope there's a better way to do this.... But unfortunately I could not find it.

Perhaps a more stable (future-proof) way of having the same behavior would be to place the input accessory view in the WKWebView.superview, then use respond to keyboard events, to show and hide it. It's the old-fashioned way of making input accessory views.
Details:
observe keyboard show/hide notifications
place the input accessory view in the WKWebView.superview
on keyboard show: position the input accessory view above the keyboard
on keyboard hide: hide it with keyboard
And perhaps obvious, use delegation to pass action events between the WKWebView and your UIViewController/UIView subclass.
Also, in iOS 13+, you can override the inputAccessoryView. You should be able to replace it to provide your own on iPad. More details here: https://stackoverflow.com/a/58001395/344591

Related

Swift button not working properly?

I have a weird bug where a buttons touchUpInside does not work as it should. I have two buttons using the same code
#IBOutlet weak var previousIBO: UIButton!
#IBOutlet weak var nextIBO: UIButton!
#IBAction func buttonDown(sender: AnyObject) {
nextSingleFire()
timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector:#selector(nextFire), userInfo: nil, repeats: true)
}
#IBAction func buttonUp(sender: AnyObject) {
timer.invalidate()
}
nextIBO.addTarget(self, action:#selector(buttonDown(sender:)), for: .touchDown)
nextIBO.addTarget(self, action:#selector(buttonUp(sender:)), for: [.touchUpInside, .touchUpOutside, .touchDragOutside])
previousIBO.addTarget(self, action:#selector(buttonDown(sender:)), for: .touchDown)
previousIBO.addTarget(self, action:#selector(buttonUp(sender:)), for: [.touchUpInside, .touchUpOutside])
However the previous button only works when I drag after tapping. as opposed to the next button that works simply by tapping. Why am I getting this weird behavior in this one button?
I would like to add.touchDragOutside to my previous button, but I can't because then the button does not work
If I were you, I'd start by removing the #IBAction on your functions.
As far as I can tell, they are useless, since you set the actions via "addTarget". And there's a chance your weird behaviour is caused by a wrong connection between your buttons and functions (select your buttons, and open the "Connections inspector" to check if everything is fine).
One other possibility is that your buttons are overlapping, or some view is blocking your button.
Also, Timers can be tricky, take a look at this post for starting and stoping them: swift invalidate timer doesn't work

Increase NavigationBar height

I have the following code:
func navbarbutton() {
UIView.animateWithDuration(0.2, animations: { () -> Void in
let current = self.navigationController?.navigationBar.frame
self.navigationController?.navigationBar.frame = CGRectMake(self.frame!.origin.x, self.frame!.origin.y, self.frame!.size.width, current!.size.height + 50)
self.navigationController?.navigationBar.layoutIfNeeded()
})
}
I'm able to increase the height of the navigation bar by 50 dp. That's not the issue for me. The issue I'm having is that the UIBarButtonItems are all aligned to the bottom. How can I get them aligned to the top so that I can add more to the bottom myself? I'm getting something as per the image:
Is it possible to get it aligned to the top?
Unfortunately you can't do that.
The view hierarchy within the UINavigationBar is private so you can't manipulate it without iterating over the subviews and going all hacky with it. This probably isn't a good idea.
Out of curiosity I looked at the Messages app in iOS 10 using the view debugger because they obviously do this. They actually achieve the layout by adding their own UIButton to replace the back and rightBarButtonItem. This is something you would be able to do however they also set the alpha of one of the internal (and private) content views to zero so that the original content is no longer visible.
This is something that you won't be able to do so easily unfortunately. You could try to hack things about until it works but remember you also need to handle pushes/pops, in call status bars, interactive transitions and rotation events.
If however you wasn't going for the iMessage style and just wanted to add some content underneath your navigation bar, why not look at pinning a UIVisualEffectView to the topLayoutGuide in your UIViewController? You can get a fairly nice look pretty easily and it saves hacking stuff about a lot. Here's an example:
Try this code:
Note: Code tested in Swift 3.
Answer 1: Updated Answer
class ViewController: UIViewController {
var customBar: UINavigationBar = UINavigationBar()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//Title
title = "Some Title"
// Add bar button item
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(addTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(addTapped))
self.customBar.frame = CGRect(x:0, y:0, width:view.frame.width, height:(navigationController?.navigationBar.frame.height)! + 50)
self.customBar.backgroundColor = UIColor.green
self.view.addSubview(customBar)
}
func addTapped() {
print("Button Pressed")
}
Output:
Answer 2:
override var isViewLoaded: Bool {
// Add bar button item
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(addTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(addTapped))
//Vertical and Horizonal barButtonItem position offset
navigationItem.leftBarButtonItem?.setTitlePositionAdjustment(UIOffset(horizontal: 0, vertical: 20), for: UIBarMetrics.default)
navigationItem.rightBarButtonItem?.setTitlePositionAdjustment(UIOffset(horizontal: 0, vertical: 20), for: UIBarMetrics.default)
return true
}
func addTapped() {
print("Button Pressed")
}
Note: Above code only works in isViewLoaded: Bool method.But, No luck.When, I tried this code in other viewLoad method.
Output 1: barButtonItem moved 20 pixel up vertically.
Output 2: barButtonItem moved 20 pixel down vertically.
Hope, Above code fix your problem.

Xcode delayed button

Hello I'm new here and new to app development but I'm building an app with multiple views and I would like to add a button that will go to the next view but the user will have to wait 3 seconds or so on each view before the button is available to be tapped.
You could perform a method after a delay with reveals or enables your button,
[self performSelector:#selector(yourMethod) withObject:nil afterDelay:3.0];
if you want the delay to happen after button tap then have a look at the below code
let tapGesture = UITapGestureRecognizer(target: self, action: "tapAction")
tapGesture.numberOfTapsRequired = 1
Your_Button_name.addGestureRecognizer(tapGesture)
#IBAction func tapAction()
{
Your_Button_name.disable=true
//Delay function to enable your button
NSTimer.scheduledTimerWithTimeInterval(Your_time_value_delay, target: self, selector: Selector("enablefunc"), userInfo: nil, repeats: false)
// DO SOMETHING WHICH YOU WANT TO DO.....
}
func enablefunc()
{
Your_Button_name.disable=false
}
Hope this helps. You can modify it based on your requirement....

Attempting to present <**> on <**> while a presentation is in progress. While performing a segue from one view controller to another using swift

override func viewDidAppear(animated: Bool) {
view.userInteractionEnabled = true
let pinchGesture:UIPinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: "pinchGesture")
view.addGestureRecognizer(pinchGesture)
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "dismissKeyboard")
view.addGestureRecognizer(tap)
}
func dismissKeyboard() {
view.endEditing(true)
}
func pinchGesture(){
self.performSegueWithIdentifier("trick1Segue", sender: self)
}
In my iOS app, i want to transition to a different view controller when a pinch gesture is performed on the screen.
The tap gesture is to dismiss the keyboard when tapped outside the keyboard area
While running the app, I get an error message:
"Attempting to present < ** > on < ** > while a presentation is in progress"
The new view controller appears but opens twice, with a very short time difference. Looked up a lot of blogs but couldn't find a solution, please help!!!
The problem is that pinchGesture can be called multiple times. You should add a property to your viewController to keep track of the fact that you already have acted upon the pinch gesture:
var segueInProcess = false
func pinchGesture() {
if !segueInProcess {
self.performSegueWithIdentifier("trick1Segue", sender: self)
segueInProcess = true
}
}
Gesture recognizers are called multiple times with different states. What you can do is check the state property of the UIPinchGestureRecognizer in pinchGesture().

UINavigationController back button title doesn't update dynamically

I have a text field in my view controller and I want to display a custom title for the back button. This is to represent changes made in the text field.
override func viewDidLoad() {
super.viewDidLoad()
// Update back button in the nav bar
updateBackButton()
// Text field delegation
nameTextField.delegate = self
nameTextField.addTarget(self, action: "updateBackButton", forControlEvents: .EditingChanged)
}
func updateBackButton() {
let backButton = UIBarButtonItem(
title: formHasChanged ? "Cancel" : "Back",
style: .Done,
target: nil,
action: nil
)
print(backButton.title)
navigationController?.navigationBar.topItem?.backBarButtonItem = backButton
}
This does effect the back button only once, in the viewDidLoad method. On subsequent calls to updateBackButton() there's no visible change, even though print(backButton.title) does print the appropriate output.
What's missing from my approach in order to have a dynamically updated back button title?
Output from the updateBackButton() method's print statement.
Optional("Back")
Optional("Cancel")
Optional("Back")
Optional("Cancel")
Optional("Back")
If you want to call a function as an action from any control's target then you'll have to define it as #IBAction and use : while calling it. Just like
override func viewDidLoad() {
super.viewDidLoad()
// Update back button in the nav bar
// updateBackButton() // Now you don't need to call it here, I guess..
// Text field delegation
nameTextField.delegate = self
nameTextField.addTarget(self, action: "updateBackButton:",forControlEvents: .EditingChanged)
}
#IBAction func updateBackButton(sender: AnyObject!) {
let backButton = UIBarButtonItem(
title: formHasChanged ? "Cancel" : "Back",
style: .Done,
target: nil,
action: nil
)
print(backButton.title!)
navigationController?.navigationBar.topItem?.backBarButtonItem = backButton
}
Update :
Try changing this line
navigationController?.navigationBar.topItem?.backBarButtonItem = backButton
with
navigationController?.navigationBar.topItem?.leftBarButtonItem = backButton or simply navigationItem.leftBarButtonItem = backButton
PS : It will not show you < symbol but I think you dont need it as you are dealing with cancel and done.
Its working now. Hope this will work for you too !

Resources