How to check if button is clicked from XIB file in Swift? - ios

I run into the following. I have created a custom UIView using XIB file. I connected a VC that inherits from UIView and add it to the File Owner property. Connect two buttons with #IBOutlet and connected both buttons to the same #IBAction method, that will switch the background color, to get a boolean effect. By default no button is selected.
In my ViewController.swift file, that is connected with a View Controller in Storyboard, I add it as subview inside my UICollectionView cells.
Based on my two model classes (Messages and PermissionMessage), I determine what the text of the buttons should be.
My wish is to add functions to the buttons, so when button one is clicked, it will fire function A and if button two is selected, function B.
At this point I don't know where I should do this logic, to determine which function should be fired off. Also because I can create different instances of the same BooleanView.xib so I can't add the functions to the file owners class of that XIB file, am I right?
I am trying to get the button click inside the didSelectItemAt method of UICollectionView, but I can't get access to that programmatically added custom UIView called BooleanView().
How and what should be the correct way to add functions to each instance buttons, based on the button that is clicked?
In my example I want to determine for the current instance if the selected button is Manual or GPS. If Manual, it will do something, like print a text in my console, if GPS is selected, I would like to call the initializer for the LocationManager that I stored in a custom separate class/handler.
This function is placed inside the cellForItemAt method of UICollectionView:
if message is PermissionMessage {
let bool = BooleanView()
bool.frame = CGRect(x: 60, y: estimatedFrame.height + 15, width: estimatedFrame.width + 15 + 8, height: bool.frame.height)
let permissionType = (message as! PermissionMessage).getTypeOfPermission()
switch permissionType {
case .camera:
print("Case: camera")
bool.leftButton.setTitle("Yes", for: .normal)
bool.rightButton.setTitle("No", for: .normal)
cell.addSubview(bool)
case .location:
print("Case: location")
bool.leftButton.setTitle("Manual", for: .normal)
bool.rightButton.setTitle("GPS", for: .normal)
cell.addSubview(bool)
}
}
Note: the message variable is the selected array entry of indexPath.row.
The custom UIView class that is added to my BooleanView.xib:
class BooleanView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var leftButton: UIButton!
#IBOutlet weak var rightButton: UIButton!
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
// Load the nib named 'CardView' into memory, finding it in the main bundle.
Bundle.main.loadNibNamed("BooleanView", owner: self, options: nil)
self.frame = contentView.frame
addSubview(contentView)
// Style the custom view
contentView.backgroundColor = UIColor.clear
leftButton.layer.cornerRadius = (leftButton.frame.height / 2)
rightButton.layer.cornerRadius = (rightButton.frame.height / 2)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
#IBAction func pressedButton(_ sender: UIButton) {
if sender.backgroundColor == Constants.BOOL_BUTTON.enabled {
return
}
sender.backgroundColor = Constants.BOOL_BUTTON.enabled
sender.titleLabel?.font = UIFont(name: Constants.FONTS.bold, size: (sender.titleLabel?.font.pointSize)!)
if sender.tag == 0 {
self.rightButton.backgroundColor = Constants.BOOL_BUTTON.disabled
self.rightButton.titleLabel?.font = UIFont(name: Constants.FONTS.regular, size: (self.rightButton.titleLabel?.font.pointSize)!)
} else {
self.leftButton.backgroundColor = Constants.BOOL_BUTTON.disabled
self.leftButton.titleLabel?.font = UIFont(name: Constants.FONTS.regular, size: (self.leftButton.titleLabel?.font.pointSize)!)
}
}
}

First you need a way to communicate which, if either, button has been pressed, an enum will work for that:
enum BooleanViewState {
case none
case left
case right
}
Next your BooleanView needs to be able to communicate its state to interested parties. Given that you only care about the state of a given boolean view at specific moments in time the easiest is to use a computed property. Also, your current method of reaching into the view to set button titles isn't the best of practices so we will add an alternative to that as well:
class BooleanView: UIView {
var boolViewState: BooleanViewState {
get {
if leftButton.backgroundColor == Constants.BOOL_BUTTON.enabled {
return .left
} else if rightButton.backgroundColor == Constants.BOOL_BUTTON.enabled {
return .right
} else {
return .none
}
}
}
var buttonTitles = (leftTitle: "", rightTitle: "") {
didSet {
leftButton.setTitle(buttonTitles.leftTitle, for: .normal)
rightButton.setTitle(buttonTitles.rightTitle, for: .normal)
}
}
// The rest of you existing class
}
It is possible you will need to add a didSet to the outlets for the two buttons that gets the titles. The titles might get set before the buttons exist.
Next you need to switch from using a default UICollectionViewCell to a custom subclass that can hold a BooleanView and pass things back and forth:
class BooleanViewCell: UICollectionViewCell {
#IBOutlet private var boolView: BooleanView!
var boolViewState: BooleanViewState {
get {
return boolView.boolViewState
}
}
var buttonTitles = (leftTitle: "", rightTitle: "") {
didSet {
boolView.buttonTitles = buttonTitles
}
}
}
You'll need to change some code in your cellForItemAt method
if message is PermissionMessage {
let permissionType = (message as! PermissionMessage).getTypeOfPermission()
switch permissionType {
case .camera:
print("Case: camera")
cell.buttonTitles = (leftTitle: "Yes", rightTitle: "No")
case .location:
print("Case: location")
cell.buttonTitles = (leftTitle: "Manual", rightTitle: "GPS")
}
}
The last thing to do is read boolState from the cell in didSelectItemAt
guard let cell = collectionView.cellForItem(at: indexPath) as? BooleanViewCell else { return }
let theState = cell.boolState
// React to the state as needed
This does mean that you will need to add a generic UIView to your prototype collection view cell in the storyboard, set its class to BooleanView and connect it to the outlet in BooleanViewCell.
That should take care of it all for you though I did all of this with no compiler so I make no guarantees I haven't mistyped or missed something all together.

Related

UICollectionViewListCell and custom cell accessory in iOS 14

My issues with new collection view list cells is that I'm not able to add action handlers to a custom accessory view.
I've been trying to do the following:
protocol TappableStar: class {
func onStarTapped(_ cell: UICollectionViewCell)
}
class TranslationListCell: UICollectionViewListCell {
let starButton: UIButton = {
let starButton = UIButton()
let starImage = UIImage(systemName: "star")!.withRenderingMode(.alwaysTemplate)
starButton.setImage(starImage, for: .normal)
starButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
starButton.addTarget(self, action: #selector(starButtonPressed(_:)), for: .touchUpInside)
starButton.tintColor = .systemOrange
return starButton
}()
var translation: TranslationModel?
weak var link: TappableStar?
override init(frame: CGRect) {
super.init(frame: frame)
accessories = [.customView(configuration: .init(customView: starButton, placement: .trailing(displayed: .always)))]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc private func starButtonPressed(_ sender: UIButton) {
link?.onStarTapped(self)
}
override func updateConfiguration(using state: UICellConfigurationState) {
// Create new configuration object and update it base on state
var newConfiguration = TranslationContentConfiguration().updated(for: state)
// Update any configuration parameters related to data item
newConfiguration.inputText = translation?.inputText
newConfiguration.outputText = translation?.outputText
contentConfiguration = newConfiguration
}
}
I subclass UICollectionViewListCell, create a button with target-action handler and add it to accessories array. I also have my own implementation of cell configuration.
Now, I create a protocol where I delegate action handling to my view controller (I also implemented new cell registration API and set cell.link = self).
My problem here is that my accessory button doesn't call starButtonPressed although this accessory view is responsive (it changes color when highlighted).
My idea is that there might be something wrong with the way I implement my action handling with a custom accessory but there seems to be little to none information about this new api.
Moreover, when choosing between predefined accessories, some of them have actionHandler closures of type UICellAccessory.ActionHandler but I don't seem to understand how to properly implement that.
Any ideas would be much appreciated.
iOS 14, using UIActions
Since iOS 14 we can initialise UIButton and other UIControls with primary actions. It becomes similar to handlers of native accessories. With this we can use any parametrized method we want. And parametrising is important, because usually we want to do some action with specific cell. #selector's cannot be parametrised, so we can't pass any information to method about which cell is to be updated.
But this solution works only for iOS 14+.
Creating UIAction:
let favoriteAction = UIAction(image: UIImage(systemName: "star"),
handler: { [weak self] _ in
guard let self = self else { return }
self.handleFavoriteAction(for: your_Entity)
})
Creating UIButton:
let favoriteButton = UIButton(primaryAction: favoriteAction)
Creating accessory:
let favoriteAccessory = UICellAccessory.CustomViewConfiguration(
customView: favoriteButton,
placement: .leading(displayed: .whenEditing)
)
Using
cell.accessories = [.customView(configuration: favoriteAccessory)]
I solved my issue by adding tap gesture recognizer to my accessory's custom view. So it works like this:
let customAccessory = UICellAccessory.CustomViewConfiguration(
customView: starButton,
placement: .trailing(displayed: .always))
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(starButtonPressed(_:)))
customAccessory.customView.addGestureRecognizer(tapGesture)
accessories = [.customView(configuration: customAccessory)]
Haven't seen it documented anywhere so hope it helps somebody.
I followed a similar approach to yours, but instead of a UITapGestureRecognizer, I added a target to the button.
var starButton = UIButton(type: .contactAdd)
starButton.addTarget(self, action: #selector(self.starButtonTapped), for: .touchUpInside)
let customAccessory = UICellAccessory.CustomViewConfiguration(customView: starButton, placement: .trailing(displayed: .always))
cell.accessories = [.customView(configuration: customAccessory)]
I first tried the tap gesture recognizer and it didn't work for me.

How do I make two UIButtons perform like radio buttons in Swift?

I have two UIButtons that I want to use to set an A/B value to a variable before I save data to a database. I want a button to become selected when tapped, and deselected when the other button is tapped, and vice versa. What is a good solution for accomplishing this programmatically or in Interface Builder?
In order to set an "A/B value" as you mention, the easiest option would be to use a UISwitch or -in the general case of possibly more than 2 options- a UISegmentedControl (as #rmaddy suggested in the question's comments) .
These controls have built-in the "choose just one out of many" functionality that you are looking for.
The drawbacks of the switch are:
It has to be either on or off (does not support a selection state of "neither A nor B")
You can't have separate title labels for each state.
If you still want two separate UIButton instances, you can:
Have references to both buttons in your view controller (#IBOutlets wired using Interface Builder), e.g.:
#IBOutlet weak var leftButton: UIButton!
#IBOutlet weak var rightButton: UIButton!
Implement the action method for both buttons in such a way that it sets the selected state of the tapped button, and resets the other one. For example:
#IBAction func buttonAction(sender: UIButton) {
if sender == leftButton {
leftButton.isSelected = true
rightButton.isSelected = false
} else if sender == rightButton{
leftButton.isSelected = false
rightButton.isSelected = true
}
}
This is a quick-and-dirty solution for just two buttons. If you want a generic radio group of n-buttons, there are open source solutions on GitHub, etc...
Try this.
First create both button separate #IBOutlet.
#IBOutlet weak var btnYes: UIButton!
#IBOutlet weak var btnNo: UIButton!
Set Both Button Tag Like this and you also set tag using storyboard.
override func viewDidLoad() {
super.viewDidLoad()
btnYes.tag = 1
btnNo.tag = 2
}
Implement Common #IBAction method for both buttons
#IBAction func btnYesNoTapped(_ sender: UIButton) {
if sender.tag == 1 {
self.IsBtnSelected(isSelect: true, with: self.btnYes)
}else {
self.IsBtnSelected(isSelect: true, with: self.btnNo)
}
}
Create Custome Method
func IsBtnSelected(isSelect:Bool,with sender:UIButton){
self.btnYes.isSelected = false
self.btnNo.isSelected = false
sender.isSelected = isSelect
}
you can use following function for creating a radio button behaviour, you have to btn outlet to be selected and array of both outlets to this function. instead ofcolor you can also compare images and set images. for getting a required value yo can create a variable in viewcontroller and assign this variable a value in IBAction of btn and you can call this function from IBAction.
func radioButton(_ btnToBeSelected: UIButton, _ btnArray: [UIButton]) {
for btn in btnArray {
if btn == btnToBeSelected {
btn.backgroundColor = UIColor.black
//selected btn
//You can also set btn images by
//btn.setImage(<#T##image: UIImage?##UIImage?#>, for: <#T##UIControlState#>)
} else {
btn.backgroundColor = UIColor.red
//not selected btn
}
}
}
In iOS , you would have to do it manually.See the below approaches,
Use a switch . Using a UISwitch would be better if the option indicates a on/off state.
Use a same method when the button is pressed. Whenever the method gets called deselect the other button/buttons and select the pressed button. You can use tags or keep a reference of the buttons to differentiate between them.
Lastly , keep different methods for each buttons . Just deselect the other buttons whenever the button is pressed.
You can follow the above approaches by using interface builder or programmatically.
You can achieve it like below
I have implemented it for dates which are in TableView you just need to do little modifications
enum filterDateSelectableOptions:Int {
case AssignDate
case DueDate
case CompletionDate
}
//Assign Date selected by default
var currentSelectedFilterDate:filterDateSelectableOptions = .AssignDate
Now
func btnRadioButtonTapped(sender:UIButton) {
switch sender.tag {
case kTableViewRow.AssignDate.rawValue:
self.currentSelectedFilterDate = .AssignDate
case kTableViewRow.DueDate.rawValue:
self.currentSelectedFilterDate = .DueDate
case kTableViewRow.CompletionDate.rawValue :
self.currentSelectedFilterDate = .CompletionDate
default:
break;
}
//sender.isSelected = !sender.isSelected
self.tblFilterList.reloadData()
}
in cellForRow I have
// THIS IS DIFFERENT ENUM SO +1 is required in my case
case .AssignDate,.DueDate,.CompletionDate :
let button = buttonRadioCircle
button.tag = row.rawValue
cell.accessoryView = button
button.addTarget(self, action: #selector(btnRadioButtonTapped(sender:)), for: .touchUpInside)
button.isSelected = self.currentSelectedFilterDate.rawValue + 1 == row.rawValue
}

Cannot pass 'var' by 'init' into '#IBAction'

I created a pair of xib file with the swift file(for that xib file).
[xib_template.xib & view_template.swift]
And I want to control this pair of xib file by my [main_VC.swift].
xib file have 1 button and 1 label.
I want to change the text of label when I click this button.
I want to set different template view and control them in my [main_VC].
But the #IBAction seems independent inside the class
I pass the value from [main_VC] to [view_template.swift] by init method searched on the internet.
I can get correct value by using func in [main_VC].
But when clicking the button,
the value is always nil.
The var inside IBAction cannot get the value from init.
I am new in swift and I tried my best but still cannot fix this.
How can I get the init value inside IBAction?
Or how can I programmatically create & disable the Ibaction from [main_VC]?
I adjusted my code to be more easy to read.
May have some little typing error.
I searched online and tried all I can already.
One people asked similar question before but have no answer.
Hope for help.
Thanks very much.
[view_template.swift]
import UIKit
class View_template_empty: UIView {
var _uid: String?
#IBOutlet weak var labellabel: UILabel!
init (uid: String) {
self._uid = uid
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
}
required init?(coder aDecoder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
super.init(coder: aDecoder)
}
#IBAction func clickingPanel2(_ sender: Any) {
print(self._uid) // always nil !!!!!!
self.labellabel.text = “test”
}
fun test () {
print(self._uid) // correct value
}
}
[main_VC] (only copy out the main function)
func allocator (_uid: String, uiView: UIView) {
switch templateType {
case “one”:
if let loading_panels = Bundle.main.loadNibNamed("xib_template", owner: uiView, options: nil)?.first as? view_template {
loading_panels.translatesAutoresizingMaskIntoConstraints = false
uiView.addSubview(loading_panels)
loading_panels.leadingAnchor.constraint(equalTo: uiView.leadingAnchor).isActive = true
loading_panels.trailingAnchor.constraint(equalTo: uiView.trailingAnchor).isActive = true
loading_panels.bottomAnchor.constraint(equalTo: uiView.bottomAnchor).isActive = true
loading_panels.topAnchor.constraint(equalTo: uiView.topAnchor).isActive = true
let view_temp = view_template(uid: _uid)
view_temp.test()
}
case “two”:
if let loading_panels = Bundle.main.loadNibNamed("xib_template_two”, owner: uiView, options: nil)?.first as? view_template_two {
loading_panels.translatesAutoresizingMaskIntoConstraints = false
uiView.addSubview(loading_panels)
loading_panels.leadingAnchor.constraint(equalTo: uiView.leadingAnchor).isActive = true
loading_panels.trailingAnchor.constraint(equalTo: uiView.trailingAnchor).isActive = true
loading_panels.bottomAnchor.constraint(equalTo: uiView.bottomAnchor).isActive = true
loading_panels.topAnchor.constraint(equalTo: uiView.topAnchor).isActive = true
}
default:
print("error")
}
You are using different initializers here:
When you say let view_temp = view_template(uid: _uid), init (uid: String) is used and your implementation sets _uid so it is not nil.
When you load a view from a XIB, init?(coder aDecoder: NSCoder) is used and this does not set _uid so it is nil.
To inject _uid into your templates, simply say loading_panels._uid = _uid in your two if let loading_panels = ... blocks.
You might also want to read section "Follow case conventions" in the Swift API Design Guidelines to brush up on your naming.

A Swift example of Custom Views for Data Input (custom in-app keyboard)

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.

UIView class for drop down list with UIButtons - delegate/protocol issue?

I am trying to create a custom drop down list in a ViewController. There are going to be 5 drop down lists and each list will have 4 options. Because of the number of lists, I decided to make a UIView that has the four choices in the form of UIButtons for each of the lists. Right now I am just trying to get one down; therefore, the following code is for ONE drop down list with FIVE options (including the one selected, which I will explain further below).
Essentially what I want is to have a button showing the selected value (or a default value at launch) and then when you click on that value then the UIView that contains 4 buttons (aka the drop down list) is shown below the original button. When the user clicks on one of the buttons I want the the button with the selected value to have the title of the button that was clicked on.
I am having the following issues:
I want to be able to pass the titles of the four buttons from the ViewController to the UIView because I want to use this UIView multiple times with different values for the titles of the four buttons. I don't know how to pass values to a UIView class.
When a choice from the drop down list (ie a UIButton) is clicked I can't figure out how to pass the value of the title of the button from the UIView back to UIViewController. I tried setting the title to a variable in the ViewController but that didn't work (showed up as nil).
Thank you so much in advance - I know this is a long questions and I am really unsure if this is even a good approach to take for what I am trying to do but it made sense in my head.
Here is my code for the ViewController
var buttonsLeft: buttonsView = buttonsView() // this is the UIView subclass
var time = UIButton.buttonWithType(UIButtonType.System) as! UIButton
override func viewWillAppear(animated: Bool) {
//hidden drop down list
self.buttonsLeft.frame = CGRect(x: self.view.bounds.width*(1/6) - 50, y:120, width:100, height: 135)
self.buttonsLeft.hidden = true
//button with selection showing or the default value at launch
self.time.frame = CGRectMake(self.view.bounds.width * (1/6) - 50, 90, 100, 30)
self.time.setTitle("1 DAY", forState: UIControlState.Normal)
self.time.addTarget(self, action: "showLeft", forControlEvents: UIControlEvents.TouchUpInside)
self.time.hidden = false
self.view.addSubview(self.time)
}
//this function shows the list
func showLeft(){
self.view.addSubview(self.buttonsLeft)
self.buttonsLeft.hidden = false
}
Here is the code for the UIView buttonsView:
import UIKit
class buttonsView: UIView {
var option1 = UIButton()
var option2 = UIButton()
var option3 = UIButton()
var option4 = UIButton()
var buttons: Array<UIButton> = Array()
var title:String = String()
override init(frame: CGRect) {
super.init(frame: frame)
self.buttons = [option1, option2, option3, option4]
self.option1.setTitle("1 DAY", forState: UIControlState.Normal)
self.option2.setTitle("1 MONTH", forState: UIControlState.Normal)
self.option3.setTitle("1 YEAR", forState: UIControlState.Normal)
self.option4.setTitle("LONGER", forState: UIControlState.Normal)
var yStep = 35
for var i:Int = 0; i < 4; ++i {
var totalY:CGFloat = CGFloat(i*yStep)
buttons[i].frame = CGRectMake(0, totalY, 100, 30)
buttons[i].addTarget(self, action: "choseOption:", forControlEvents: UIControlEvents.TouchUpInside)
buttons[i].hidden = false
self.addSubview(buttons[i])
}
}
func choseOption(sender:UIButton){
self.title = sender.titleLabel!.text!
MyView().parentTitle = sender.titleLabel!.text! // my attempt at assigning to variable in View Controller
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Delegation will help you to pass value to UIViewController.
Here are the way you can implement delegate in swift.
Step 1 : Declare protocol in class which is used to sending data. here is buttonsview.
#objc protocol MyButtonDelegate{
optional func didSelectButton(text:String)
}
Step 2 : Now declare delegate in sending class. here is buttonsview.
class buttonsView: UIView {
var delegate:MyButtonDelegate?
[other stuf......]
}
Step 3: now use delegate to send data to 'UIViewController'.
func choseOption(sender:UIButton){
delegate!.didSelectButton(text: sender.titleLabel!.text!)
}
Step 4 : adopt protocol in receiving class.
class ViewController: UIViewController,MyButtonDelegate {
Step 5: implement delegate method in receiving class.
func didSelectButton(text: String) {
parentTitle = "The Buttons title is " + text
}
Step 6: now set delegate
override func viewDidLoad() {
super.viewDidLoad()
buttonsLeft.delegate = self
}
Hope this help you.

Resources