search
Happy Tutorial Tuesday! Last week I had to create a keyboard extension for iOS that displays images and a search bar to be able to search for certain images. I looked up several tutorials, everything seemed straight forward… Until I started typing in the search and I couldn’t get the focus back on the the third party app (msg / or any other app). The nightmare began! I was so sure there was a “clean” way to do it that I spend days looking for it. I was wrong. So I looked for a workaround. What if I fake the UISearchBar (or UITextField) with a UILabel that will contain a little blinking view to recreate the blinking cursor when on focus. This is what I’m going to show you today: How to be able to search for something on your keyboard and focus back on the third party app.

1- Set up the project and the Keyboard Extension

Go ahead and create a new project ( File -> New -> Project). Choose the single view app, give it a name. Tada, you have an app! Now let’s attach a keyboard extension to it. Go to File->New->Target -> Keyboard Extension. Click Next and give your Keyboard a name.You will be prompt to a pop up asking you if you want to activate the Keyboard scheme. Go ahead and activate it, this will be useful to debug your keyboard.

1
Screen Shot 2015-11-23 at 3.58.58 PM

Go ahead and run your app on the Simulator. Be careful to choose the app and not the keyboard in the target:
Screen Shot 2015-11-23 at 4.14.10 PM
Now you need to add it in the settings on your Simulator like you would had it on your phone. Close Safari and navigate to the Settings -> General -> Keyboards -> Add New Keyboard … And choose the keyboard you just created.
Screen Shot 2015-11-23 at 4.07.04 PM
Now go back into Xcode and rerun the project but this time choose the Keyboard as the target and not the app.
Screen Shot 2015-11-23 at 4.04.13 PM
If you run it using Safari once the keyboard is running tap on the Safari Search Bar and the regular keyboard will appear. Tap on the globe icon. You should see your keyboard.
Screen Shot 2015-11-23 at 4.13.36 PM
Your keyboard is pretty simple at this stage. You only have one button called “Next Keyboard.” Note that this functionality is mandatory for any keyboard extension.

Since we all know XCode is garbage, you may have to add your keyboard in the setting more than once during the process because you will see the the keyboard might disappear from the list. Don’t ask me why…

Since I don’t wanna recreate a full keyboard I’m just going to put one letter to be able to type in the searchbar.
Just add this code in your viewDidLoad() bellow the code that is already there.

let button = createButtonWithTitle("A")
self.view.addSubview(button)

And add this function bellow your code:

func createButtonWithTitle(title: String) -> UIButton {
     let button = UIButton(type: .System) as UIButton
     button.frame = CGRectMake(0, 0, 20, 20)
     button.setTitle(title, forState: .Normal)
     button.sizeToFit()
     button.titleLabel!.font = UIFont.systemFontOfSize(15)
     button.translatesAutoresizingMaskIntoConstraints = false
     button.backgroundColor = UIColor(white: 1.0, alpha: 1.0)
     button.setTitleColor(UIColor.darkGrayColor(), forState: .Normal)
     button.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
 
     return button
}

If you want more info on how to create a “real keyboard” in your keyboard extension, I highly recommend this Tutorial : http://www.appdesignvault.com/ios-8-custom-keyboard-extension/

Your KeyboardViewController.swift should be:

//
//  KeyboardViewController.swift
//  keyboard
//
//  Created by Auriga on 11/23/15.
//  Copyright © 2015 Oskoui+Oskoui. All rights reserved.
//
 
import UIKit
 
class KeyboardViewController: UIInputViewController {
 
    @IBOutlet var nextKeyboardButton: UIButton!
 
    override func updateViewConstraints() {
        super.updateViewConstraints()
 
        // Add custom view sizing constraints here
    }
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        // Perform custom UI setup here
        self.nextKeyboardButton = UIButton(type: .System)
 
        self.nextKeyboardButton.setTitle(NSLocalizedString("Next Keyboard", comment: "Title for 'Next Keyboard' button"), forState: .Normal)
        self.nextKeyboardButton.sizeToFit()
        self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = false
 
        self.nextKeyboardButton.addTarget(self, action: "advanceToNextInputMode", forControlEvents: .TouchUpInside)
 
        self.view.addSubview(self.nextKeyboardButton)
 
        let nextKeyboardButtonLeftSideConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Left, relatedBy: .Equal, toItem: self.view, attribute: .Left, multiplier: 1.0, constant: 0.0)
        let nextKeyboardButtonBottomConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Bottom, relatedBy: .Equal, toItem: self.view, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
        self.view.addConstraints([nextKeyboardButtonLeftSideConstraint, nextKeyboardButtonBottomConstraint])
 
 
        let button = createButtonWithTitle("A")
        self.view.addSubview(button)
    }
 
    func createButtonWithTitle(title: String) -> UIButton {
        let button = UIButton(type: .System) as UIButton
        button.frame = CGRectMake(0, 0, 20, 20)
        button.setTitle(title, forState: .Normal)
        button.sizeToFit()
        button.titleLabel!.font = UIFont.systemFontOfSize(15)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = UIColor(white: 1.0, alpha: 1.0)
        button.setTitleColor(UIColor.darkGrayColor(), forState: .Normal)
        button.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
 
        return button
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated
    }
 
    override func textWillChange(textInput: UITextInput?) {
        // The app is about to change the document's contents. Perform any preparation here.
    }
 
    override func textDidChange(textInput: UITextInput?) {
        // The app has just changed the document's contents, the document context has been updated.
 
        var textColor: UIColor
        let proxy = self.textDocumentProxy
        if proxy.keyboardAppearance == UIKeyboardAppearance.Dark {
            textColor = UIColor.whiteColor()
        } else {
            textColor = UIColor.blackColor()
        }
        self.nextKeyboardButton.setTitleColor(textColor, forState: .Normal)
    }
 
    func didTapButton(sender: AnyObject?) {
        let button = sender as! UIButton
        let title = button.titleForState(.Normal)
        let proxy = textDocumentProxy as UITextDocumentProxy
        proxy.insertText(title!)
    }
 
}

2- Set up the UILabel to fake the searchBar or UITextField

Now in order to recreate a UITExtField I’ll need a UILabel and a UIView for the blinking cursor. I’m also gonna add a boolean is searching and a done button to be able to switch the focus between the third party app and my keyboard.
I also created a small custom class for UILabel to add some padding to the left of the Label. I added it below the KeyboardViewController but to be clean you should create a new file for it.

My KeyboardViewController.swift is now:

//
//  KeyboardViewController.swift
//  keyboard
//
//  Created by Auriga on 11/23/15.
//  Copyright © 2015 Oskoui+Oskoui. All rights reserved.
//
 
import UIKit
 
class KeyboardViewController: UIInputViewController {
 
    @IBOutlet var nextKeyboardButton: UIButton!
 
    var searchBar:CustomLabel!
    var blinkCursor:UIView!
    var isSearching:Bool = false
    var doneButton:UIButton!
 
    override func updateViewConstraints() {
        super.updateViewConstraints()
 
        // Add custom view sizing constraints here
    }
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        // Perform custom UI setup here
        self.nextKeyboardButton = UIButton(type: .System)
 
        self.nextKeyboardButton.setTitle(NSLocalizedString("Next Keyboard", comment: "Title for 'Next Keyboard' button"), forState: .Normal)
        self.nextKeyboardButton.sizeToFit()
        self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = false
 
        self.nextKeyboardButton.addTarget(self, action: "advanceToNextInputMode", forControlEvents: .TouchUpInside)
 
        self.view.addSubview(self.nextKeyboardButton)
 
        let nextKeyboardButtonLeftSideConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Left, relatedBy: .Equal, toItem: self.view, attribute: .Left, multiplier: 1.0, constant: 0.0)
        let nextKeyboardButtonBottomConstraint = NSLayoutConstraint(item: self.nextKeyboardButton, attribute: .Bottom, relatedBy: .Equal, toItem: self.view, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
        self.view.addConstraints([nextKeyboardButtonLeftSideConstraint, nextKeyboardButtonBottomConstraint])
 
 
        let button = createButtonWithTitle("A")
        self.view.addSubview(button)
 
 
        self.searchBar = CustomLabel(frame:CGRect(x: 80, y: 0, width: 210, height: 30))
        self.searchBar.backgroundColor = UIColor.whiteColor()
        self.searchBar.userInteractionEnabled = true
        self.searchBar.text = "Search"
        self.searchBar.textColor = UIColor.lightGrayColor()
        self.searchBar.layer.borderWidth = 1.0
        self.searchBar.layer.cornerRadius = 2.0
        self.searchBar.layer.borderColor = UIColor.blackColor().CGColor
        self.searchBar.addGestureRecognizer(UITapGestureRecognizer(target: self, action:Selector("handleTapSearchBar:")))
 
        self.blinkCursor = UIView(frame: CGRect(x: 3, y: 5, width: 2, height: self.searchBar.frame.height - 10))
        self.blinkCursor.backgroundColor = UIColor(red:0.98, green:0.62, blue:0.00, alpha:1.0)
        self.searchBar.addSubview(blinkCursor)
        self.blinkCursor.tag = 100
        self.blinkCursor.hidden = true
 
        _ = NSTimer.scheduledTimerWithTimeInterval(0.6, target: self, selector: "updateBlinkCursor", userInfo: nil, repeats: true)
 
 
        self.searchBar.addSubview(blinkCursor)
        self.view.addSubview(searchBar)
 
        self.doneButton = UIButton(type: .Custom)
        self.doneButton.frame = CGRect(x: 10, y: 40, width: 50, height: 20)
        self.doneButton.setTitle("Done", forState: .Normal)
        self.doneButton.addTarget(self, action: "doneSearching:", forControlEvents: .TouchUpInside)
        self.view.addSubview(doneButton)
 
    }
 
    func handleTapSearchBar(recognizer: UITapGestureRecognizer) {
        self.blinkCursor.hidden = false
        self.searchBar.text = ""
        self.isSearching = true
    }
 
    func doneSearching(sender: AnyObject?) {
        self.blinkCursor.hidden = true
        self.searchBar.text = "Search"
        self.isSearching = false
    }
 
    func updateBlinkCursor(){
        if(self.blinkCursor.tag == 100){
            self.blinkCursor.tag = 101
            self.blinkCursor.backgroundColor = UIColor.clearColor()
        }else{
            self.blinkCursor.tag = 100
            self.blinkCursor.backgroundColor = UIColor(red:0.98, green:0.62, blue:0.00, alpha:1.0)
        }
    }
 
 
    func createButtonWithTitle(title: String) -> UIButton {
        let button = UIButton(type: .System) as UIButton
        button.frame = CGRectMake(0, 0, 20, 20)
        button.setTitle(title, forState: .Normal)
        button.sizeToFit()
        button.titleLabel!.font = UIFont.systemFontOfSize(15)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = UIColor(white: 1.0, alpha: 1.0)
        button.setTitleColor(UIColor.darkGrayColor(), forState: .Normal)
        button.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
 
        return button
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated
    }
 
    override func textWillChange(textInput: UITextInput?) {
        // The app is about to change the document's contents. Perform any preparation here.
    }
 
    override func textDidChange(textInput: UITextInput?) {
        // The app has just changed the document's contents, the document context has been updated.
 
        var textColor: UIColor
        let proxy = self.textDocumentProxy
        if proxy.keyboardAppearance == UIKeyboardAppearance.Dark {
            textColor = UIColor.whiteColor()
        } else {
            textColor = UIColor.blackColor()
        }
        self.nextKeyboardButton.setTitleColor(textColor, forState: .Normal)
    }
 
    func didTapButton(sender: AnyObject?) {
        let button = sender as! UIButton
        let title = button.titleForState(.Normal)
        let proxy = textDocumentProxy as UITextDocumentProxy
        proxy.insertText(title!)
    }
 
}
 
class CustomLabel: UILabel {
 
    /*
    // Only override drawRect: if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func drawRect(rect: CGRect) {
    // Drawing code
    }
    */
 
    override func drawRect(rect: CGRect) {
        let insets:UIEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
        super.drawTextInRect(UIEdgeInsetsInsetRect(rect,insets))
    }
 
}

So in the code you can see I also added the tap event on the searchBar to trigger when the user starts searching. This will display the blinking cursor and set the boolean isSearching to true. We will use this Boolean next in the didTapButton function.
We are almost there!

Your keyboard should look like this:
Screen Shot 2015-11-24 at 11.02.59 AM

3- Switch the focus from the third app search to the keyboard search

The last step is suppa easy! :) You just need to change the didTapButton function so now it looks like:

    func didTapButton(sender: AnyObject?) {
        let button = sender as! UIButton
        let title = button.titleForState(.Normal)
        let proxy = textDocumentProxy as UITextDocumentProxy
 
        if(self.isSearching){
            self.searchBar.text = self.searchBar.text! + title!
            let textSize:CGSize = self.searchBar.text!.sizeWithAttributes([NSFontAttributeName: self.searchBar.font])
            self.blinkCursor.frame.origin.x = textSize.width + 4
        }else{
            proxy.insertText(title!)
        }
 
 
    }

Run your code and … Tada, it just works!!! :)

You can download the project here

I know you love me! Xoxo