Custom Input View in Swift - ios

I've spent hours trying to figure out how to create/then get a custom inputView to work.
I have a grid of TextInputs (think scrabble board) that when pressed should load a custom inputView to insert text.
I've created a .xib file containing the UI elements for the custom inputView. I was able to create a CustomInputViewController and have the inputView appear but never able to get the actual TextInput to update it's value/text.
Apple documentation has seemed light on how to get this to work and the many tutorials I've have seen have been using Obj-C (which I have been unable to convert over due to small things that seem unable to now be done in swift).
What is the overarching architecture and necessary pieces of code that should be implemented to create a customInputView for multiple textInputs (delegate chain, controller, .xibs, views etc)?

Set up a nib file with the appropriate inputView layout and items. In my case I set each button to an action on File Owner of inputViewButtonPressed.
Set up a storyboard (or nib if you prefer) for a view controller.
Then using the following code, you should get what you're looking for:
class ViewController: UIViewController, UITextFieldDelegate {
var myInputView : UIView!
var activeTextField : UITextField?
override func viewDidLoad() {
super.viewDidLoad()
// load input view from nib
if let objects = NSBundle.mainBundle().loadNibNamed("InputView", owner: self, options: nil) {
myInputView = objects[0] as UIView
}
// Set up all the text fields with us as the delegate and
// using our input view
for view in self.view.subviews {
if let textField = view as? UITextField {
textField.inputView = myInputView
textField.delegate = self
}
}
}
func textFieldDidBeginEditing(textField: UITextField) {
activeTextField = textField
}
func textFieldDidEndEditing(textField: UITextField) {
activeTextField = nil
}
#IBAction func inputViewButtonPressed(button:UIButton) {
// Update the text field with the text from the button pressed
activeTextField?.text = button.titleLabel?.text
// Close the text field
activeTextField?.resignFirstResponder()
}
}
Alternatively, if you're wanting to be more keyboard-like, you can use this function as your action (it uses the new let syntax from Swift 1.2), break it up if you need 1.1 compatibility:
#IBAction func insertButtonText(button:UIButton) {
if let textField = activeTextField, title = button.titleLabel?.text, range = textField.selectedTextRange {
// Update the text field with the text from the button pressed
textField.replaceRange(range, withText: title)
}
}
This uses the UITextInput protocol to update the text field as appropriate. Handling delete is a little more complicated, but still not too bad:
#IBAction func deleteText(button:UIButton) {
if let textField = activeTextField, range = textField.selectedTextRange {
if range.empty {
// If the selection is empty, delete the character behind the cursor
let start = textField.positionFromPosition(range.start, inDirection: .Left, offset: 1)
let deleteRange = textField.textRangeFromPosition(start, toPosition: range.end)
textField.replaceRange(deleteRange, withText: "")
}
else {
// If the selection isn't empty, delete the chars in the selection
textField.replaceRange(range, withText: "")
}
}
}

You shouldn't go through all that hassle. There's a new class in iOS 8 called: UIAlertController where you can add TextFields for the user to input data. You can style it as an Alert or an ActionSheet.
Example:
let alertAnswer = UIAlertController(title: "Input your scrabble Answer", message: nil, preferredStyle: .Alert) // or .ActionSheet
Now that you have the controller, add fields to it:
alertAnswer.addTextFieldWithConfigurationHandler { (get) -> Void in
getAnswer.placeholder = "Answer"
getAnswer.keyboardType = .Default
getAnswer.clearsOnBeginEditing = true
getAnswer.borderStyle = .RoundedRect
} // add as many fields as you want with custom tweaks
Add action buttons:
let submitAnswer = UIAlertAction(title: "Submit", style: .Default, handler: nil)
let cancel = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
alertAnswer.addAction(submitAnswer)
alertAnswer.addAction(cancel)
Present the controller whenever you want with:
self.presentViewController(alertAnswer, animated: true, completion: nil)
As you see, you have various handlers to pass custom code at any step.
As example, this is how it would look:

Related

How to modify UIMenu before it's shown to support dynamic actions

iOS 14 adds the ability to display menus upon tapping or long pressing a UIBarButtonItem or UIButton, like so:
let menu = UIMenu(children: [UIAction(title: "Action", image: nil) { action in
//do something
}])
button.menu = menu
barButtonItem = UIBarButtonItem(title: "Show Menu", image: nil, primaryAction: nil, menu: menu)
This most often replaces action sheets (UIAlertController with actionSheet style). It's really common to have a dynamic action sheet where actions are only included or may be disabled based on some state at the time the user taps the button. But with this API, the menu is created at the time the button is created. How can you modify the menu prior to it being presented or otherwise make it dynamic to ensure the appropriate actions are available and in the proper state when it will appear?
You can store a reference to your bar button item or button and recreate the menu each time any state changes that affects the available actions in the menu. menu is a settable property so it can be changed any time after the button is created. You can also get the current menu and replace its children like so: button.menu = button.menu?.replacingChildren([])
For scenarios where you are not informed when the state changes for example, you really need to be able to update the menu right before it appears. There is a UIDeferredMenuElement API which allows the action(s) to be generated dynamically. It's a block where you call a completion handler providing an array of UIMenuElement. A placeholder with loading UI is added by the system and is replaced once you call the completion handler, so it supports asynchronous determination of menu items. However, this block is only called once and then it is cached and reused so this doesn't do what we need for this scenario. iOS 15 added a new uncached provider API which behaves the same way except the block is invoked every time the element is displayed, which is exactly what we need for this scenario.
barButtonItem.menu = UIMenu(children: [
UIDeferredMenuElement.uncached { [weak self] completion in
var actions = [UIMenuElement]()
if self?.includeTestAction == true {
actions.append(UIAction(title: "Test Action") { [weak self] action in
self?.performTestAction()
})
}
completion(actions)
}
])
Before this API existed, I did find for UIButton you can change the menu when the user touches down via target/action like so: button.addTarget(self, action: #selector(buttonTouchedDown(_:)), for: .touchDown). This worked only if showsMenuAsPrimaryAction was false so they had to long press to open the menu. I didn't find a solution for UIBarButtonItem, but you could use a UIButton as a custom view.
After some trial, I've found out that you can modify the UIButton 's .menu by setting the menu property to null first then set the new UIIMenu
here is the sample code that I made
#IBOutlet weak var button: UIButton!
func generateMenu(max: Int, isRandom: Bool = false) -> UIMenu {
let n = isRandom ? Int.random(in: 1...max) : max
print("GENERATED MENU: \(n)")
let items = (0..<n).compactMap { i -> UIAction in
UIAction(
title: "Menu \(i)",
image: nil
) {[weak self] _ in
guard let self = self else { return }
self.button.menu = nil // HERE
self.button.menu = self.generateMenu(max: 10, isRandom: true)
print("Tap")
}
}
let m = UIMenu(
title: "Test", image: nil,
identifier: UIMenu.Identifier(rawValue: "Hello.menu"),
options: .displayInline, children: items)
return m
}
override func viewDidLoad() {
super.viewDidLoad()
button.menu = generateMenu(max: 10)
button.showsMenuAsPrimaryAction = true
}
Found a solution for the case with UIBarButtonItem. My solution is based on Jordan H solution, but I am facing a bug - my menu update method regenerateContextMenu() was not called every time on menu appears, and I was getting irrelevant data in the menu. So I changed the code a bit:
private lazy var threePointBttn: UIButton = {
$0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
// pay attention on UIControl.Event in next line
$0.addTarget(self, action: #selector(regenerateContextMenu), for: .menuActionTriggered)
$0.showsMenuAsPrimaryAction = true
return $0
}(UIButton(type: .system))
override func viewDidLoad() {
super.viewDidLoad()
threePointBttn.menu = createContextMenu()
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: threePointBttn)
}
private func createContextMenu() -> UIMenu {
let action1 = UIAction(title:...
// ...
return UIMenu(title: "Some title", children: [action1, action2...])
}
#objc private func regenerateContextMenu() {
threePointBttn.menu = createContextMenu()
}
tested on iOS 14.7.1
Modified Jordan H's version to separate the assignment and build action
This will build the menu on the fly every time the button is tapped
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem?.menu = UIMenu(children: [
// build menu every time the button is tapped
UIDeferredMenuElement.uncached { [weak self] completion in
if let menu = self?.buildMenu() as? UIMenu {
completion([menu])
}
}
])
}
func buildMenu() -> UIMenu {
var actions: [UIMenuElement] = []
// build actions
UIAction(title: "Filter", image: UIImage(systemName: "line.3.horizontal.decrease.circle")) { _ in
self.filterTapped()
}
actions.append(filterAction)
return UIMenu(options: .displayInline, children: actions)
}

textfield.becomeFirstResponder isn't keeping text field focused in swift

I have a page where there is a UITextField that I add programmatically, I set the text type to number and add constraints and all that. Then I add a done button to the accessoryView of the text field and add a function to run when that done button is pressed. My problem is, when the page loads, I want the text field to be focused and the keyboard shown. I set the becomeFirstResponder on it, but when the page loads, the keyboard shows up for a split second then immediately disappears and the delegate methods are run.
I need to find a way to make the text field "active", "focused", whatever you want to call it when the page loads, and for the keyboard to be there and ready. I can't seem to find any help aside from call becomeFirstResponder on it, which only works for a split second.
Here is the code I am using to build the page and run everything, I simplified it to reduce clutter and read times, but if you need more info, please let me know and I will be happy to provide the full code...
class AgeViewController: UIViewController {
var selectedAge: Int = 0
var textInput: UITextField!
let settings = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
createPage()
}
override func viewWillAppear(_ animated: Bool) {
textInput.becomeFirstResponder()
//I have tried this in both viewWillAppear and viewDidAppear
}
func createPage() {
textInput = UITextField()
textInput.font = .systemFont(ofSize: 50)
textInput.placeholder = "35"
textInput.borderStyle = .none
textInput.keyboardType = .numberPad
textInput.returnKey = .done
textInput.textAlignment = .right
addDoneButton()
textInput.delegate = self
view.addSubView(textInput)
//create a label and add it to the page
}
private fun addDoneButton() {
let doneToolbar: UIToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50)
doneToolbar.barStyle = .default
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(doneTapped))
let items = [flexSpace, done]
doneToolbar.sizeToFit()
textInput.inputAccessoryView = doneToolbar
}
#objc func doneTapped() {
textInput.resignFirstResponder()
}
}
extension AgeViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.textColor = UIColor(named: "text")!
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
if textField.text != nil {
selectedAge = Int(textField.text!) ?? 35
settings.set(selectedAge, forKey: Strings.age)
} else {
textField.textColor = UIColor(named: "grayText")!
}
}
Like I said, the page loads, the keyboard shows up for a split second, then goes away and the delegate methods are called for didEndEditing. I don't understand why it isn't staying focused, I am calling becomeFirstResponder. I have tried calling textInput.becomeFirstResponder() in 3 different places, all with the same result. The first was right after I add the subview to the view, then I tried in viewDidAppear and finally in viewWillAppear, all have the same result, shows up for a split second, then goes away. Sorry for the long post, thank you for any help, I really appreciate it.

Delegate seems to not be working, according to the console

Here i have two classes. How do I make JournalPage call JournalEntryController didSubmit method.
protocol JournalPageDelegate {
func didSubmit(for commentText: String)
}
class JournalPage: UIViewController, UITextViewDelegate {
var delegate: JournalPageDelegate?
fileprivate let textView: UITextView = {
let textView = UITextView()
let attributedText = NSMutableAttributedString(string: "Enter Text Here.", attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18)])
textView.textColor = UIColor.black
textView.backgroundColor = UIColor.white
textView.becomeFirstResponder()
textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
textView.attributedText = attributedText
return textView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(save))
}
#objc func save() {
print("saving")
guard let commentText = textView.text else { return }
delegate?.didSubmit(for: commentText)
}
And here is the class where I want to call the method.
class JournalEntryController: UIPageViewController, UIPageViewControllerDataSource, JournalPageDelegate {
func didSubmit(for commentText: String) {
print("Testing for text")
}
}
And for some reason, I don't see "Testing" on the console when I tap save on JournalPage class. How do I make JournalPage call JournalEntryController didSubmit method?
Whenever you used delegates you need to pass that delegate from one view controller to another view controller.
According to Apple definition:
Delegation is a simple and powerful pattern in which one object in a program acts on behalf of, or in coordination with, another object. The delegating object keeps a reference to the other object–the delegate–and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled. The delegate may respond to the message by updating the appearance or state of itself or other objects in the application, and in some cases it can return a value that affects how an impending event is handled. The main value of delegation is that it allows you to easily customize the behavior of several objects in one central object.
The missing part you are doing is that you are not calling the delegate for expample you called JournalTextDelegate in your class JournalEntryController so you need to call this JournalTextDelegate to your JournalPage.
for example: Suppose your going to another view controller through push method
let vc = self.storyboard?.instantiateViewController(withIdentifier: “identifierier”) as! JournalPage
vc.delegate = self // you need to call this delegate
self.navigationController?.pushViewController(notifDetailVCObj, animated: true)
And it will work fine. For reference see documentation https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html
You delegate from pageview controller:
{your JournalPage controller object}.delegate = self

UITextField in UITableView not becoming first responder

I have UITableView with about 20 row which each contain a UITextField. The first time I tap in a textfield will open the keyboard and I am ready to edit this textfield. If I tap on the next textfield (notice the keyboard is displayed all the time) the keyboard is still displayed but the blue cursor is not in the new textfield and I cannot enter any text. But if I tap on another textfield again, it works just fine. This behavior occurs alternately, one time it works the other time it doesn't.
The delegate method textFieldShouldBeginEditing(_:) is always called, wether I can edit or not. The delegate method textFieldDidBeginEditing(_:) is only called when editing works.
This is the code for cellForRowAt
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextFieldCell")!
let titleLabel = cell.viewWithTag(1) as! UILabel
let contentTextField = cell.viewWithTag(2) as! FixableTextField
contentTextField.delegate = self
contentTextField.inputAccessoryView = doneToolbar
// Enable/disable editing for text fields
if isEditing {
contentTextField.enableEditing()
} else {
contentTextField.disableEditing()
}
// Present Profile Data
if profileUpdateBuffer != nil {
switch indexPath.row {
case 0:
titleLabel.text = "Count"
contentTextField.text = "\(profileUpdateBuffer!.count)"
contentTextField.purposeID = "count"
contentTextField.keyboardType = .numberPad
case 1:
titleLabel.text = "City"
contentTextField.text = "\(profileUpdateBuffer!.city)"
contentTextField.purposeID = "city"
contentTextField.keyboardType = .default
// ...
case 20:
titleLabel.text = "Name"
contentTextField.text = "\(profileUpdateBuffer!.name)"
contentTextField.purposeID = "name"
contentTextField.keyboardType = .default
default:
titleLabel.text = ""
contentTextField.text = ""
}
return cell
}
// No data available -> show info in first row
else {
if indexPath.row == 0 {
titleLabel.text = "No data"
contentTextField.text = "No data"
}
else {
titleLabel.text = ""
contentTextField.text = ""
}
return cell
}
}
The enableEditing() and disableEditing() method are from class FixableTextField. I can see that the textfields are always enabled because I can see the textfield border
// Extract from FixableTextField class
func enableEditing() {
self.isEnabled = true
self.borderStyle = .roundedRect
}
func disableEditing() {
self.isEnabled = false
self.borderStyle = .none
}
Code for the UITextField
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
// Delete empty field indicator "-"
if textField.text == "-" {
textField.text = ""
}
//Move profileTable's contentView to correct position
if textField is FixableTextField {
let path = IndexPath(row: rowMap[(textField as! FixableTextField).purposeID]!, section: 0)
moveContentViewUp(indexPath: path)
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
// Save new value to profileUpdateBuffer
do {
try self.profileUpdateBuffer?.setProperty(value: textField.text!, key: (textField as! FixableTextField).purposeID)
} catch ProfileError.PropertySettingWrongType {
let falseInputAlert = UIAlertController(title: "False Input", message: "The input for this field is not valid.", preferredStyle: .alert)
falseInputAlert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(falseInputAlert, animated: true, completion: nil)
} catch {
print("Error when trying to set property for profileUpdateBuffer in ProfileViewController")
}
// Display new data in table
profileTable.reloadData()
}
Extract from setProperty method which is from class ProfileData. profileUpdateBuffer is of type ProfileData
func setProperty(value:String, key:String) throws {
switch key {
case "count":
count = value
case "city":
count = value
// ...
case "name":
name = value
default:
throw ProfileError.PropertySettingWrongType
}
}
I've made a small program to mimic the behavior you describe.
It seems the issue is caused by table view data reloading at the end of your textFieldDidEndEditing(_:):
func textFieldDidEndEditing(_ textField: UITextField) {
// Save new value to profileUpdateBuffer
do {
try self.profileUpdateBuffer?.setProperty(value: textField.text!, key: (textField as! FixableTextField).purposeID)
} catch ProfileError.PropertySettingWrongType {
let falseInputAlert = UIAlertController(title: "False Input", message: "The input for this field is not valid.", preferredStyle: .alert)
falseInputAlert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(falseInputAlert, animated: true, completion: nil)
} catch {
print("Error when trying to set property for profileUpdateBuffer in ProfileViewController")
}
// Display new data in table
profileTable.reloadData()
}
Try removing profileTable.reloadData() for the sake of experiment to confirm the root cause of the problem (yes, your other cells will not be updated).
One way to solve this is by utilizing direct cell updates on visibleCells in textFieldDidEndEditing(_:). I see profileUpdateBuffer? is your data model. Just update your cell's titleLabel and textField properties manually from your model if they are in visible cells property of the table view.
If you want to size the cells accordingly, use AutoLayout and UITableViewAutomaticDimension for table view row height combined with beginUpdates()/endUpdates() calls.
For more details on how to achieve direct cell manipulation and/or dynamic cell size update without loosing the keyboard focus check the accepted answer on this question I've already answered.
Hope this will help!
I believe the problem is that you call profileTable.reloadData()... You should probably only reload one cell. Perhaps textFieldDidEndEditing(:) gets called with the old TextField as a parameter, followed by textFieldShouldBeginEditing(:) for the new TextField. The problem is that you refresh all the cells at the end of textFieldDidEndEditing(:), which means the TextField passed by the system as a parameter to textFieldShouldBeginEditing(:) may not necessary be the same one that the corresponding cell contains... It may be that those new key-strokes get sent to a TextField belonging to a cell that is in the Queue of reusable cells, i.e. not visible, but still existing somewhere in memory.

Popover notification view, independent of current view controller in swift

I want to notify users that an action has been completed in the background. Currently, the AppDelegate receives notification of this:
func didRecieveAPIResults(originalRequest: String, apiResponse: APIResponse) {
if(originalRequest == "actionName") {
// do something
}
}
I'd really like to display a pop over notification (e.g. "Awarded points to 10 students") over the currently active view.
I know how to do this with NSNotification, but that means I have to add a listener to each of the views. An alternative to that would be great!
The next part of question is how do I actually get the view to fade in and then fade out again in front of whatever view I have - be that a table view, collection view or whatever else. I've tried the following code (in the viewDidLoad for the sake of testing):
override func viewDidLoad() {
// set up views
let frame = CGRectMake(0, 200, 320, 200)
let notificationView = UIView(frame: frame)
notificationView.backgroundColor = UIColor.blackColor()
let label = UILabel()
label.text = "Hello World"
label.tintColor = UIColor.whiteColor()
// add the label to the notification
notificationView.addSubview(label)
// add the notification to the main view
self.view.addSubview(notificationView)
print("Notification should be showing")
// animate out again
UIView.animateWithDuration(5) { () -> Void in
notificationView.hidden = true
print("Notification should be hidden")
}
}
The view does appear without the hiding animation, but with that code in it hides straight away. I'm also not sure how to stick this to the bottom of the view, although perhaps that's better saved for another question. I assume I'm doing a few things wrong here, so any advice pointing me in the right direction would be great! Thanks!
For your notification issue, maybe UIAlertController suits your needs?
This would also solve your issues with fading in/out a UIView
func didRecieveAPIResults(originalRequest: String, apiResponse: APIResponse) {
if(originalRequest == "actionName") {
// Creates an UIAlertController ready for presentation
let alert = UIAlertController(title: "Score!", message: "Awarded points to 10 students", preferredStyle: UIAlertControllerStyle.Alert)
// Adds the ability to close the alert using the dismissViewControllerAnimated
alert.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.Cancel, handler: { action in alert.dismissViewControllerAnimated(true, completion: nil)}))
// Presents the alert on top of the current rootViewController
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
}
UIAlertController
When adding a subview you want to be on top of everything else, do this:
self.view.addSubview(notificationView)
self.view.bringSubviewToFront(notificationView)
Fading a UIView by changing the alpha directly:
For testing, you should be calling this in your viewDidAppear so that the fading animation starts after the view actually is shown.
// Hides the view
UIView.animateWithDuration(5) { () -> Void in
notificationView.alpha = 0
}
// Displays the view
UIView.animateWithDuration(5) { () -> Void in
notificationView.alpha = 0
}
This solution takes up unnecessary space in your code, I would recommend extensions for this purpose.
Extensions:
Create a Extensions.swift file and place the following code in it.
Usage: myView.fadeIn(), myView.fadeOut()
import UIKit
extension UIView {
// Sets the alpha to 0 over a time period of 0.15 seconds
func fadeOut(){
UIView.animateWithDuration(0.15, animations: {
self.alpha = 0
})
}
// Sets the alpha to 1 over a time period of 0.15 seconds
func fadeIn(){
UIView.animateWithDuration(0.15, animations: {
self.alpha = 1
})
}
}
Swift 2.1 Extensions
Hope this helps! :)

Resources