I'm using CosmicMind's Material library for swift. I'm trying to get the Menu example to work with programmatic autolayout - where can I find an example of how to make this work?
From the examples on Github I haven't figured out a way to use autolayout.
/// Prepares the FlatMenu example.
private func prepareFlatbMenuExample() {
let btn1: FlatButton = FlatButton()
btn1.addTarget(self, action: "handleFlatMenu", forControlEvents: .TouchUpInside)
btn1.setTitleColor(MaterialColor.white, forState: .Normal)
btn1.backgroundColor = MaterialColor.blue.accent3
btn1.pulseColor = MaterialColor.white
btn1.setTitle("Sweet", forState: .Normal)
view.addSubview(btn1)
let btn2: FlatButton = FlatButton()
btn2.setTitleColor(MaterialColor.blue.accent3, forState: .Normal)
btn2.borderColor = MaterialColor.blue.accent3
btn2.pulseColor = MaterialColor.blue.accent3
btn2.borderWidth = .Border1
btn2.setTitle("Good", forState: .Normal)
view.addSubview(btn2)
let btn3: FlatButton = FlatButton()
btn3.setTitleColor(MaterialColor.blue.accent3, forState: .Normal)
btn3.borderColor = MaterialColor.blue.accent3
btn3.pulseColor = MaterialColor.blue.accent3
btn3.borderWidth = .Border1
btn3.setTitle("Nice", forState: .Normal)
view.addSubview(btn3)
// Initialize the menu and setup the configuration options.
flatMenu = Menu(origin: CGPointMake(spacing, view.bounds.height - height - spacing))
flatMenu.direction = .Down
flatMenu.spacing = 8
flatMenu.buttonSize = CGSizeMake(120, height)
flatMenu.buttons = [btn1, btn2, btn3]
}
flatMenu is of type Menu from the Material library, and is a class that holds each of the buttons in the menu. It appears the "origin" is what controls the positioning on the page, but because Menu isn't a UIView subclass, I'm not sure how to use this with autolayout.
public class Menu {
...
}
What view do you have the buttons in? Try positioning the superview with AutoLayout using the same dimensions as your button size and then set the origin of the Menu as CGRectZero.
Also make sure you have clipsToBounds for the superview set to false (the default). And because the buttons are outside the bounds of your autolayout placed view, you'll need to use a custom UIView subclass like the one below to handle the actions on the buttons.
class MenuParentView: UIView {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
//because the subviews will be outside the bounds of this view
//we need to look at the subviews to see if we have a hit
for subview in self.subviews {
let pointForTargetView = subview.convertPoint(point, fromView: self)
if CGRectContainsPoint(subview.bounds, pointForTargetView) {
return subview.hitTest(pointForTargetView, withEvent: event)
}
}
return super.hitTest(point, withEvent: event)
}
}
Related
I add a custom button to the tabBar in my MyViewController.viewDidLoad(subclass of UITabBarController)
But I find it doesn't response the selector.
If I delay one second to add button(in DispatchQueue.main.asyncAfter closure) ,it works OK.
I think it's not the right way to resolve it.
func addButton() {
let button = UIButton(type: UIButton.ButtonType.custom)
button.bounds = CGRect(x:0,y:0,width:30,height:30);
button.backgroundColor = UIColor.red
button.center = CGPoint(x:self.tabBar.frame.size.width/2, y:self.tabBar.frame.size.height/2 - 20);
button.addTarget(self, action: #selector(click(button:)), for: UIControl.Event.touchUpInside)
tabBar.addSubview(button)
}
You have added button to UITabBar of UITabBarController as half of part of the button would appear above the Tabbar and half of below the Tabbar as per frame.
So I guess you will not get click on part of that button which is out of Tabbar(above Tabbar) would not get touch. I you will make button little big OR try to click with arrow in simulator, you will get idea.
If you need to have button at bottom but slightly upper, then please create custom Tabbar to achieve design like this. Or else you can add that button into UITabBarController’s view instead of Tabbar.
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.addButton()
}
func addButton() {
let button = UIButton(type: UIButton.ButtonType.custom)
button.bounds = CGRect(x:0,y:0,width:50,height:50); //1
button.backgroundColor = UIColor.purple
button.center = CGPoint(x:self.tabBar.frame.size.width/2, y:self.tabBar.frame.size.height/2 - 50 + self.tabBar.frame.origin.y); //2
button.addTarget(self, action: #selector(click(button:)), for: UIControl.Event.touchUpInside)
button.layer.cornerRadius = button.frame.size.height/2
button.layer.masksToBounds = false
button.layer.shadowColor = UIColor.black.withAlphaComponent(0.5).cgColor
button.layer.shadowRadius = 5.0
button.layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
button.layer.shadowOpacity = 0.5
//tabBar.addSubview(button) //3
self.view.addSubview(button). //4
}
#objc func click(button: UIButton) {
print("Button get clicked")
}
}
I have marked four things with commented by numbers at the end of lines, that you can make to your code and try.
I'm tired that UIButton's image is too complicated to customize. And it's long to create tappable UIImageView. Than i want to create own mixed element of UIImage and UIButton. It would let to create custom animation, touch areas and lot more.
I decided to take a UIImageView and add a gesture or a button to it to detect touches. But i need to have a TouchUpInside or just touch event to easily connect it in IB. Look at the code, what is already written:
class ClickableImageView: UIImageView {
let embeddedButton = UIButton()
override func awakeFromNib() {
super.awakeFromNib()
setup()
}
fileprivate func setup() {
isUserInteractionEnabled = true
sv(embeddedButton)
embeddedButton.fillContainer()
embeddedButton.addTarget(self, action: #selector(didPress), for: .touchUpInside)
}
#objc fileprivate func didPress() {
embeddedButton.preventRepeatedPresses()
}
}
I did the same with the UIButton subclassing and it works, but it not showing the image in IB, so it confuse the developer.
The last problem to solve is how to add the event?
UIButton image can be easily resized by giving distance from (top,left,bottom and right) of the UIButton like below.
yourButton.imageEdgeInsets = UIEdgeInsetsMake(10, 50, 10, 95)
If you are looking for adding an event to the UIImageView, do the following,
yourImage.isUserInteractionEnabled = true
let tapImage = UITapGestureRecognizer(target: self, action: #selector(myFunction(tapGestureRecognizer:)))
yourImage.addGestureRecognizer(tapImage)
Then handle your method,
#objc func myFunction(tapGestureRecognizer: UITapGestureRecognizer) {
//do something here
}
I'm trying to get/keep a handle on elements in my UIStackView after I have moved it with a pan gesture.
For example, the elements I am trying to grab a handle on are things like the button tag or the text label text.
The code explained…
I am creating a UIStackView, via the function func createButtonStack(label: String, btnTag: Int) -> UIStackView
It contains a button and a text label.
When the button stack is created, I attach a pan gesture to it so I can move the button around the screen. The following 3 points work.
I get the button stack created
I can press the button and call the fun to print my message.
I can move the button stack.
The issue I have is…
Once I move the button stack the first time and the if statement gesture.type == .ended line is triggered, I lose control of the button stack.
That is, the button presses no longer work nor can I move it around any longer.
Can anyone please help? Thanks
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
let ButtonStack = createButtonStack(label: “Button One”, btnTag: 1)
view.addSubview(ButtonStack)
ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
ButtonStack.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
let panGuesture = UIPanGestureRecognizer(target: self, action: #selector(pan(guesture:)))
ButtonStack.isUserInteractionEnabled = true
ButtonStack.addGestureRecognizer(panGuesture)
}
func createButtonStack(label: String, btnTag: Int) -> UIStackView {
let button = UIButton()
button.setImage( imageLiteral(resourceName: "star-in-circle"), for: .normal)
button.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
button.contentMode = .scaleAspectFit
button.tag = btnTag
switch btnTag {
case 1:
button.addTarget(self, action: #selector(printMessage), for: .touchUpInside)
case 2:
break
default:
break
}
//Text Label
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.green
textLabel.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
textLabel.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
textLabel.font = textLabel.font.withSize(15)
textLabel.text = label
textLabel.textAlignment = .center
//Stack View
let buttonStack = UIStackView()
buttonStack.axis = UILayoutConstraintAxis.vertical
buttonStack.distribution = UIStackViewDistribution.equalSpacing
buttonStack.alignment = UIStackViewAlignment.center
buttonStack.spacing = 1.0
buttonStack.addArrangedSubview(button)
buttonStack.addArrangedSubview(textLabel)
buttonStack.translatesAutoresizingMaskIntoConstraints = false
return buttonStack
}
#objc func printMessage() {
print(“Button One was pressed”)
}
#objc func pan(guesture: UIPanGestureRecognizer) {
let translation = guesture.translation(in: self.view)
if let guestureView = guesture.view {
guestureView.center = CGPoint(x: guestureView.center.x + translation.x, y: guestureView.center.y + translation.y)
if guesture.state == .ended {
print("Guesture Center - Ended = \(guestureView.center)")
}
}
guesture.setTranslation(CGPoint.zero, in: self.view)
}
If you're using autolayout on the buttonStack you can't manipulate the guestureView.center centerX directly. You have to work with the constraints to achieve the drag effect.
So instead of ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true you should do something along the lines of:
let centerXConstraint = ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
centerXConstraint.isActive = true
ButtonStack.centerXConstraint = centerXConstraint
To do it like this you should declare a weak property of type NSLayoutConstraint on the ButtonStack class. You can do the same thing for the centerY constraint.
After that in the func pan(guesture: UIPanGestureRecognizer) method you can manipulate the centerXConstraint and centerYConstraint properties directly on the ButtonStack view.
Also, I see you are not setting the translatesAutoresizingMaskIntoConstraints property to false on the ButtonStack. You should do that whenever you are using autolayout programatically.
Thanks to PGDev and marosoaie for their input. Both provided insight for me to figure this one out.
My code worked with just the one button, but my project had three buttons inside a UIStackView.
Once I moved one button, it effectively broke the UIStackView and I lost control over the moved button.
The fix here was to take the three buttons out of the UIStackView and I can now move and control all three buttons without issues.
As for keeping a handle on the button / text field UIStackView, this was achieved by adding a .tag to the UIStackView.
Once I moved the element, the .ended action of the pan could access the .tag and therefore allow me to identify which button stack was moved.
Thanks again for all of the input.
I am using cosmicmind material swift library and am following the examples code to try to get the FAB MenuView working.
I have copied the code and added the buttons i want, to test i am just testing with 2 buttons. The problem I am facing is with the handleMenu function:
/// Handle the menuView touch event.
internal func handleMenu() {
if menuView.menu.opened {
menuView.close()
(menuView.menu.views?.first as? MaterialButton)?.animate(MaterialAnimation.rotate(rotation: 0))
} else {
menuView.menu.open() { (v: UIView) in
(v as? MaterialButton)?.pulse()
}
(menuView.menu.views?.first as? MaterialButton)?.animate(MaterialAnimation.rotate(rotation: 0.125))
}
}
The full code for this UINavigationController:
import UIKit
import Material
class MyTeeUpsController: UINavigationController {
/// MenuView reference.
private lazy var menuView: MenuView = MenuView()
/// Default spacing size
let spacing: CGFloat = 16
/// Diameter for FabButtons.
let diameter: CGFloat = 56
/// Handle the menuView touch event.
internal func handleMenu() {
if menuView.menu.opened {
menuView.close()
(menuView.menu.views?.first as? MaterialButton)?.animate(MaterialAnimation.rotate(rotation: 0))
} else {
menuView.menu.open() { (v: UIView) in
(v as? MaterialButton)?.pulse()
}
(menuView.menu.views?.first as? MaterialButton)?.animate(MaterialAnimation.rotate(rotation: 0.125))
}
}
/// Handle the menuView touch event.
internal func handleButton(button: UIButton) {
print("Hit Button \(button)")
}
private func prepareMenuView() {
//let w: CGFloat = 52
var img:UIImage? = MaterialIcon.cm.add?.imageWithRenderingMode(.AlwaysTemplate)
let button1: FabButton = FabButton()//frame: CGRectMake((view.bounds.width - w)-10, 550,w,w))
button1.setImage(img, forState: .Normal)
button1.setImage(img, forState: .Highlighted)
button1.pulseColor = MaterialColor.blue.accent3
button1.backgroundColor = MaterialColor.blueGrey.lighten1
button1.borderColor = MaterialColor.blue.accent3
button1.borderWidth = 1
button1.addTarget(self, action: #selector(handleMenu), forControlEvents: .TouchUpInside)
menuView.addSubview(button1)
img = UIImage(named: "filing_cabinet")?.imageWithRenderingMode(.AlwaysTemplate)
let button2:FabButton = FabButton()
button2.depth = .None
button2.setImage(img, forState: .Normal)
button2.setImage(img, forState: .Highlighted)
button2.pulseColor = MaterialColor.blue.accent3
button2.borderColor = MaterialColor.blue.accent3
button2.borderWidth = 1
button2.backgroundColor = MaterialColor.blueGrey.lighten1
button2.addTarget(self, action: #selector(handleButton), forControlEvents: .TouchUpInside)
menuView.addSubview(button2)
menuView.menu.direction = .Up
menuView.menu.baseSize = CGSizeMake(diameter, diameter)
menuView.menu.views = [button1,button2]
view.layout(menuView).width(diameter).height(diameter).bottomRight(bottom: 58, right: 20)
}
private func prepareTabBarItem() {
//todo
}
override func viewDidLoad() {
super.viewDidLoad()
prepareMenuView()
}
}
The menu I have embedded as a subView of UINavigationController. The reason I have added to this subView is because the FAB is on top of a search/display controller (TableView) and this way the FAB can remain on top of the TableView even when scrolling the contents of the Table.
When the view initially loads, I can click on the menu button and the animation happens correctly and button2 appears. However, it does not allow me to hit the second button OR close the menu by pressing button1 again UNLESS I navigate to another tab in the tab bar controller and then navigate back to the tab where the FAB MenuView was located. I am loading my prepareMenuView() function in viewDidLoad just as it is shown in the example.
Not sure how to modify this so that it can behave as desired. It doesn't make sense to pick another ViewController lifecycle method to run prepareMenuView().
so the issue with your code is that button2 only has the selector handler for handleButton. The handleMenu handler is not added to it. So you have two solutions.
Add the handleMenu call to the handleButton
internal func handleButton(button: UIButton) {
print("Hit Button \(button)")
handleMenu(button)
}
Add a selector handler to the button2 instance for handleMenu.
button2.addTarget(self, action: #selector(handleMenu), forControlEvents: .TouchUpInside)
button2.addTarget(self, action: #selector(handleButton), forControlEvents: .TouchUpInside)
Either option will work, just remember that order matters. So if you want the menu to close before you load some content, then call the method before or add the selector handler handleMenu before you add the handleButton.
:) All the best!
Goal
I want to make a custom keyboard that is only used within my app, not a system keyboard that needs to be installed.
What I have read and tried
Documentation
App Extension Programming Guide: Custom Keyboard
Custom Views for Data Input
The first article above states:
Make sure a custom, systemwide keyboard is indeed what you want to
develop. To provide a fully custom keyboard for just your app or to
supplement the system keyboard with custom keys in just your app, the
iOS SDK provides other, better options. Read about custom input views
and input accessory views in Custom Views for Data Input in Text
Programming Guide for iOS.
That is what led me to the second article above. However, that article did not have enough detail to get me started.
Tutorials
iOS 8: Creating a Custom Keyboard in Swift
How to make a custom keyboard in iOS 8 using Swift
Xcode 6 Tutorial: iOS 8.0 Simple Custom Keyboard in Swift
Creating a Custom Keyboard Using iOS 8 App Extension
I was able to get a working keyboard from the second tutorial in the list above. However, I couldn't find any tutorials that showed how to make an in app only keyboard as described in the Custom Views for Data Input documentation.
Stack Overflow
I also asked (and answered) these questions on my way to answering the current question.
How to input text using the buttons of an in-app custom keyboard
Delegates in Swift
Question
Does anyone have a minimal example (with even one button) of an in app custom keyboard? I am not looking for a whole tutorial, just a proof of concept that I can expand on myself.
This is a basic in-app keyboard. The same method could be used to make just about any keyboard layout. Here are the main things that need to be done:
Create the keyboard layout in an .xib file, whose owner is a .swift file that contains a UIView subclass.
Tell the UITextField to use the custom keyboard.
Use a delegate to communicate between the keyboard and the main view controller.
Create the .xib keyboard layout file
In Xcode go to File > New > File... > iOS > User Interface > View to create the .xib file.
I called mine Keyboard.xib
Add the buttons that you need.
Use auto layout constraints so that no matter what size the keyboard is, the buttons will resize accordingly.
Set the File's Owner (not the root view) to be the Keyboard.swift file. This is a common source of error. See the note at the end.
Create the .swift UIView subclass keyboard file
In Xcode go to File > New > File... > iOS > Source > Cocoa Touch Class to create the .swift file.
I called mine Keyboard.swift
Add the following code:
import UIKit
// The view controller will adopt this protocol (delegate)
// and thus must contain the keyWasTapped method
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
}
class Keyboard: UIView {
// This variable will be set as the view controller so that
// the keyboard can send messages to the view controller.
weak var delegate: KeyboardDelegate?
// MARK:- keyboard initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeSubviews()
}
func initializeSubviews() {
let xibFileName = "Keyboard" // xib extention not included
let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
self.addSubview(view)
view.frame = self.bounds
}
// MARK:- Button actions from .xib file
#IBAction func keyTapped(sender: UIButton) {
// When a button is tapped, send that information to the
// delegate (ie, the view controller)
self.delegate?.keyWasTapped(character: sender.titleLabel!.text!) // could alternatively send a tag value
}
}
Control drag from the buttons in the .xib file to the #IBAction method in the .swift file to hook them all up.
Note that the protocol and delegate code. See this answer for a simple explanation about how delegates work.
Set up the View Controller
Add a UITextField to your main storyboard and connect it to your view controller with an IBOutlet. Call it textField.
Use the following code for the View Controller:
import UIKit
class ViewController: UIViewController, KeyboardDelegate {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// initialize custom keyboard
let keyboardView = Keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 300))
keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
// replace system keyboard with custom keyboard
textField.inputView = keyboardView
}
// required method for keyboard delegate protocol
func keyWasTapped(character: String) {
textField.insertText(character)
}
}
Note that the view controller adopts the KeyboardDelegate protocol that we defined above.
Common error
If you are getting an EXC_BAD_ACCESS error, it is probably because you set the view's custom class as Keyboard.swift rather than do this for the nib File's Owner.
Select Keyboard.nib and then choose File's Owner.
Make sure that the custom class for the root view is blank.
The key is to use the existing UIKeyInput protocol, to which UITextField already conforms. Then your keyboard view need only to send insertText() and deleteBackward() to the control.
The following example creates a custom numeric keyboard:
class DigitButton: UIButton {
var digit: Int = 0
}
class NumericKeyboard: UIView {
weak var target: (UIKeyInput & UITextInput)?
var useDecimalSeparator: Bool
var numericButtons: [DigitButton] = (0...9).map {
let button = DigitButton(type: .system)
button.digit = $0
button.setTitle("\($0)", for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.addTarget(self, action: #selector(didTapDigitButton(_:)), for: .touchUpInside)
return button
}
var deleteButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("⌫", for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = "Delete"
button.addTarget(self, action: #selector(didTapDeleteButton(_:)), for: .touchUpInside)
return button
}()
lazy var decimalButton: UIButton = {
let button = UIButton(type: .system)
let decimalSeparator = Locale.current.decimalSeparator ?? "."
button.setTitle(decimalSeparator, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
button.setTitleColor(.black, for: .normal)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor.darkGray.cgColor
button.accessibilityTraits = [.keyboardKey]
button.accessibilityLabel = decimalSeparator
button.addTarget(self, action: #selector(didTapDecimalButton(_:)), for: .touchUpInside)
return button
}()
init(target: UIKeyInput & UITextInput, useDecimalSeparator: Bool = false) {
self.target = target
self.useDecimalSeparator = useDecimalSeparator
super.init(frame: .zero)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Actions
extension NumericKeyboard {
#objc func didTapDigitButton(_ sender: DigitButton) {
insertText("\(sender.digit)")
}
#objc func didTapDecimalButton(_ sender: DigitButton) {
insertText(Locale.current.decimalSeparator ?? ".")
}
#objc func didTapDeleteButton(_ sender: DigitButton) {
target?.deleteBackward()
}
}
// MARK: - Private initial configuration methods
private extension NumericKeyboard {
func configure() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
addButtons()
}
func addButtons() {
let stackView = createStackView(axis: .vertical)
stackView.frame = bounds
stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(stackView)
for row in 0 ..< 3 {
let subStackView = createStackView(axis: .horizontal)
stackView.addArrangedSubview(subStackView)
for column in 0 ..< 3 {
subStackView.addArrangedSubview(numericButtons[row * 3 + column + 1])
}
}
let subStackView = createStackView(axis: .horizontal)
stackView.addArrangedSubview(subStackView)
if useDecimalSeparator {
subStackView.addArrangedSubview(decimalButton)
} else {
let blank = UIView()
blank.layer.borderWidth = 0.5
blank.layer.borderColor = UIColor.darkGray.cgColor
subStackView.addArrangedSubview(blank)
}
subStackView.addArrangedSubview(numericButtons[0])
subStackView.addArrangedSubview(deleteButton)
}
func createStackView(axis: NSLayoutConstraint.Axis) -> UIStackView {
let stackView = UIStackView()
stackView.axis = axis
stackView.alignment = .fill
stackView.distribution = .fillEqually
return stackView
}
func insertText(_ string: String) {
guard let range = target?.selectedRange else { return }
if let textField = target as? UITextField, textField.delegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == false {
return
}
if let textView = target as? UITextView, textView.delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: string) == false {
return
}
target?.insertText(string)
}
}
// MARK: - UITextInput extension
extension UITextInput {
var selectedRange: NSRange? {
guard let textRange = selectedTextRange else { return nil }
let location = offset(from: beginningOfDocument, to: textRange.start)
let length = offset(from: textRange.start, to: textRange.end)
return NSRange(location: location, length: length)
}
}
Then you can:
textField.inputView = NumericKeyboard(target: textField)
That yields:
Or, if you want a decimal separator, too, you can:
textField.inputView = NumericKeyboard(target: textField, useDecimalSeparator: true)
The above is fairly primitive, but it illustrates the idea: Make you own input view and use the UIKeyInput protocol to communicate keyboard input to the control.
Also please note the use of accessibilityTraits to get the correct “Spoken Content” » “Speak Screen” behavior. And if you use images for your buttons, make sure to set accessibilityLabel, too.
Building on Suragch's answer, I needed a done and backspace button and if you're a noob like me heres some errors you might encounter and the way I solved them.
Getting EXC_BAD_ACCESS errors?
I included:
#objc(classname)
class classname: UIView{
}
fixed my issue however Suragch's updated answer seems to solve this the more appropriate/correct way.
Getting SIGABRT Error?
Another silly thing was dragging the connections the wrong way, causing SIGABRT error. Do not drag from the function to the button but instead the button to the function.
Adding a Done Button
I added this to the protocol in keyboard.swift:
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
func keyDone()
}
Then connected a new IBAction from my done button to keyboard.swift like so:
#IBAction func Done(sender: UIButton) {
self.delegate?.keyDone()
}
and then jumped back to my viewController.swift where i am using this keyboard and added this following after the function keyWasTapped:
func keyDone() {
view.endEditing(true)
}
Adding Backspace
This tripped me up a lot, because you must set the textField.delegate to self in the viewDidLoad() method (shown later).
First: In keyboard.swift add to the protocol func backspace():
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
func keyDone()
func backspace()
}
Second: Connect a new IBAction similar to the Done action:
#IBAction func backspace(sender: UIButton) {
self.delegate?.backspace()
}
Third: Over to the viewController.swift where the NumberPad is appearing.
Important: In viewDidLoad() set all textFields that will be using this keyboard. So your viewDidLoad() should look something like this:
override func viewDidLoad() {
super.viewDidLoad()
self.myTextField1.delegate = self
self.myTextField2.delegate = self
// initialize custom keyboard
let keyboardView = keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 240))
keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
// replace system keyboard with custom keyboard
myTextField1.inputView = keyboardView
myTextField2.inputView = keyboardView
}
I'm not sure how to, if there is a way to just do this to all textFields that are in the view. This would be handy...
Forth: Still in viewController.swift we need to add a variable and two functions. It will look like this:
var activeTextField = UITextField()
func textFieldDidBeginEditing(textField: UITextField) {
print("Setting Active Textfield")
self.activeTextField = textField
print("Active textField Set!")
}
func backspace() {
print("backspaced!")
activeTextField.deleteBackward()
}
Explanation of whats happening here:
You make a variable that will hold a textField.
When the "textFieldDidBeginEditing" is called it sets the variable so it knows which textField we are dealing with. I've added a lot of prints() so we know everything is being executed.
Our backspace function then checks the textField we are dealing with and uses .deleteBackward(). This removes the immediate character before the cursor.
And you should be in business.
Many thanks to Suragchs for helping me get this happening.