What does UISearchBar.value for key do in swift? - swift

When using a UITableView and UISearchBar in swift, I was trying to find a way to keep the cancel button on the search bar enabled when the user searches something then scrolls in the table view, so they don't have to click twice to cancel the search. The default behaviour for resigning the first responder (or ending editing on the search bar) will gray out the cancel button, but I wanted to keep it enabled, so on Stackoverflow I found how to do this, but I can't find an answer online as to what searchBar.value(forKey: "cancelButton") does in the code below. Obviously it's somehow creating a reference to the cancel button on the search bar, but I don't understand what .value does (as I'm new to swift), and where the "cancelButton" key is coming from. I have read the func value(forKey key: String) documentation, but I still didn't understand it. It would be great if someone could explain what this is doing.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
searchBar.resignFirstResponder()
// If the user scrolls, keep the cancel button enabled
if let cancelButton = searchBar.value(forKey: "cancelButton") as? UIButton { // <-- This line
if searchBar.text != "" { cancelButton.isEnabled = true }
else { searchBar.showsCancelButton = false }
}
}
Thanks in advance.

UISearchBar is a subclass of
UIView -> UIResponder -> NSObject
And all NSObjects are conforming the NSKeyValueCoding Protocol Reference
valueForKey: is a KVC method. It works with ANY NSObject class and anything else that conforms to the above protocol. valueForKey: allows you to access a property using a string for its name. So for instance, if I have an Account class with a property number, I can do the following:
let myAccount = Account(number: 12)
myAccount.value(forKey: "number")
Since it is a runtime check, it can't be sure what the return type will be. So you have to cast it manually like:
let number = myAccount.value(forKey: "number") as? Int
I'm not going to explain the downcast and optionals here
So you can access any property of an object that conforms to NSKeyValueCoding just by knowing its method's exact name (that can be found easily by a simple reverse engineering).
Also, there is a similar method called performSelector that lets you execute any function of the object
⚠️ But be aware that Apple will reject your app if you touch a private variable or function of a system. (If they found out!)
⚠️ Also, be aware that any of these can be renamed without notice and your app will face undefined behaviors.

I was running into the same problem as you for searching through a list but I realized you can implement UITextfields instead...
Using didReturn to do textfield.resignFirstResponder() and when it resigns to take the value using textfield.value to search through a list.

In searchbar for iOS 12 or below, to access the elements you can use key value to access the elements. Like this function -
private func configureSearchBar() {
searchBar.barTintColor = Color.navBarColor
searchBar.makeRounded(cornerRadius: 5, borderWidth: 1, borderColor: Color.navBarColor)
searchBar.placeholder = searchBarPlaceholderText
searchBar.setImage(Images.search_white, for: .search, state: .highlighted)
searchBar.setImage(Images.green_check, for: .clear, state: .normal)
if let textField = searchBar.value(forKey: "searchField") as? UITextField {
textField.textColor = .white
textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "", attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
let leftSideImage = textField.leftView as? UIImageView
leftSideImage?.tintColor = .white
}
if let cancelButton = searchBar.value(forKey: "cancelButton") as? UIButton {
cancelButton.setTitleColor(.white, for: .normal)
}
}
Here by using the keys we are accessing the elements of the searchbar.

Related

Different behavior between addTarget and addGestureRecognizer

I have a function that creates a button with a selector function as a target. The address of a button gets passed to handleSelectPhoto.
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto))
func createButton(selector: Selector) -> UIButton {
let button = UIButton(type: .system)
button.addTarget(self, action: selector, for: .touchUpInside)
return button
}
#objc func handleSelectPhoto(button: UIButton) {
// Do something with button, this works
}
Now, I am trying to change the class of the above from UIButton to UIImageView like the following,
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto))
func createButton(selector: Selector) -> UIImageView {
let view = UIImageView()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: selector))
view.isUserInteractionEnabled = true
return view
}
#objc func handleSelectPhoto(button: UIImageView) {
// HERE, button does not get passed
}
With the above changes, in handleSelectPhoto, button instance is not correct. I can not read it as UIImageView type.
If I add a selector function using addGestureRecognizer, does it behave differently than adding a selector function using addTarget, in terms of how selector function is executed with parameters? Maybe I am not understanding how this selector function works...
Adding a target to something like UIGestureRecognizer or UIButton only passes one parameter to the selected function. This parameter depends on the type you are about to add the target on.
In your case the first code snippet works because you are adding a target to an UIButton, so your selected function gets passed this UIButton instance.
In your second scenario you add the target to an UITapGestureRecognizer, so the passed instance will be exactly this gesture recognizer, which cannot be of type UIImageView.
So the difference from the target parameter perspective between UIGestureRecognizer and UIButton is no difference. They both pass their instances to the selected function.
From the UIView subclass perspective there is the difference that UIGestureRecognizer is not a subclass of UIView, but UIButton is. That's why you can just use the passed UIButton instance in your first snippet. In the second snippet you need use the view property of UIGestureRecognizer.
guard let imageView = gestureRecognizer.view as? UIImageView else { return }
Besides your actual question it seems important to clarify how to write #selectors correctly. You're doing it correct already. No change necessary. Some may say you need to add (_:) or : to your selector like so: #selector(handleSelectPhoto(_:)) but this isn't true. In general, you only need to add these special characters when you are selecting a method which has an overload method with a different amount of parameters, but the same base name.
You should make your tell while setting the selection that your function will accept a parameter by adding : at the end of method name.
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto:))
UIKit will automatically understand that the selector methods parameter will be of type UITapGestureRecognizer. Now rewrite the below method like this and you will be good to go.
#objc func handleSelectPhoto(gesture: UITapGestureRecognizer) {
if let buttonImageView = gesture.view as? UIImageView {
//Here you can make changes in imageview what ever you want.
}
}

Gesture Recognizer and UIImageView With Custom Property

My app uses images which can have various statuses so I am using custom properties as tags. This works ok, but my tap gesture recognizer can't seem to access these properties. When the image is tapped, I need the action to depend on the state of these properties. Is there a way the gesture recognizer can read these custom properties from the tapped subclassed UIImageView or should I take a different approach? Thanks!
public class advancedTagUIImageView: UIImageView {
var photoViewedStatus: Bool?
var photoLikedStatus: Bool?
}
viewDidLoad() {
let imageView = advancedTagUIImageView(frame:CGRect(origin: CGPoint(x:50, y:50), size: CGSize(width:100,height:100)))
imageView.image = UIImage(named: dog.png)
imageView.photoViewedStatus = false
imageView.photoLikeStatus = false
imageView.tag = 7
imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(soundTapped)))
view.addSubview(imageView)
}
#objc func soundTapped(gesture: UIGestureRecognizer) {
let photoTag = gesture.view!.tag // this works great
let isPhotoLiked = gesture.view!.photoLikeStatus // this doesn't work
// do whatever
}
Swift is strongly typed. The type of the gesture.view property is UIView which doesn't have the properties defined in your advancedTagUIImageView class. This is because you could theoretically also attach your UITapGestureRecognizer to any other type of view. In which case the program would crash on your soundTapped method, because you're just assuming that gesture.view is an advancedTagUIImageView which might not always be the case.
For the compiler to let you access these properties you need first check if gesture.view is really your sublcass like this:
if let photoView = (gesture.view? as? advancedTagUIImageView) {
// you can access your tags here
let isPhotoLiked = photoView.photoLikeStatus
} else {
// you might want to handle the case that the gesture was invoked from another view. If you're certain this should not happen, maybe just throw an assertion error to get notified in case it still does.
}
PS: According to the Swift API Design Guidelines type names should be capitalized, so in your case it should be AdvancedTagUIImageView. Not following these guidelines might not crash your program, but doing so might make your life a lot easier should you ever need to write code together with other people.

Can I change outlets using an array, instead of hard coding everything?

I have an app where a user can select a number of different buttons onscreen. When a user selects a button, it turns green and the text will be used in a later view. I am trying to make everything nice and swift by minimising the amount of code I am writing.
Every button is connected to the same action and their identity is determined by their tag. What I have done is created 2 arrays to track the card name and their on/off state. When a card is pressed the cardPressed function is called, this decides whether to turn the card green or white currently (it will do more later).
What I want to do is to perform the colour change in one line of code, instead of
cardOne.backgroundColor = UIColor.white
I want to do this [#1]
cardList[cardNumber].backgroundColor = UIColor.green
so that my outlet changes depending on the selection made. I would normally just have a massive switch statement that would read like so
switch cardList[cardNumber] {
case 0:
cardOne.backgroundColor = UIColor.green
case 1:
cardTwo.backgroundColor = UIColor.green
case 2:
cardThree.backgroundColor = UIColor.green
case So on so forth:
cardInfinity.......
default:
break
}
Obviously when I try to do [#1] I get an error because it is a string, not an outlet connection. What I would like to know, is there anyway to trick xcode into recognising it as an outlet, or better yet have a way to change the outlets I am acting upon in one line of code?
Hopefully I haven't rambled too much and you can understand my thought process! I have included all of the relevant code below, obviously it won't compile. If you have any ideas they would be appreciated, or if I'm being too optimistic and this isnt possible, just let me know :) for now I will be using a big switch statement! (maybe this is useful to me in the future!)
Thanks!
private let cardList = ["cardOne","cardTwo","cardThree"]
private var cardState = [false, false, false]
//Card functions
private func selectCard(cardNumber: Int){
cardState[cardNumber] = true
cardList[cardNumber].backgroundColor = UIColor.green
}
private func deselectCard(cardNumber: Int){
cardState[cardNumber] = false
//cardOne.backgroundColor = UIColor.white
}
//Decide which function to perform, based on the card information recieved
private func cardPressed(cardNumber: Int){
let selectedCardName = cardList[cardNumber]
let selectedCardState = cardState[cardNumber]
print("\(selectedCardName)")
print("\(selectedCardState)")
switch selectedCardState {
case true:
deselectCard(cardNumber: cardNumber)
case false:
selectCard(cardNumber: cardNumber)
}
}
//UI Connections
//Card button actions
#IBAction func buttonPressed(_ sender: UIButton) {
//Determine which button has been pressed
//let cardName = sender.currentTitle!
let cardSelection = sender.tag - 1
cardPressed(cardNumber: cardSelection)
}
//Card button outlets
#IBOutlet weak var cardOne: UIButton!
#IBOutlet weak var cardTwo: UIButton!
The solution lies in the wonderful world of object-oriented programming. Instead of using parallel arrays, you can create your own data type to group this data and behavior together.
If you created your own UIButton subclass, you could keep track of whether the button is selected with your own custom property, and make visual modifications as needed.
class CardButton: UIButton {
var isChosen: Bool = false {
didSet { backgroundColor = isChosen ? UIColor.green : UIColor.white }
}
}
If you set the buttons in the storyboard to be your new CardButton type, you can use their isChosen property in code.
Your buttonPressed function could look like this instead:
#IBAction func buttonPressed(_ sender: CardButton) {
sender.isChosen = !sender.isChosen
}
This would allow you to remove the majority of your existing code, since the data is stored inside each of your buttons.

How to make a number pad appear without a text box

Hello I am trying to have a number pad appear after a timer is up. Then have my user type numbers on the pad and their input be saved to a variable in my code not a text box. I can't seem to find anything on popping up a number pad without using a text box. Any help is appreciated.
Ok I'm going to give you some code that will greatly help you. You need some sort of UITextView or UITextField to get the system keyboard. So essentially what we will do is have a textField without showing it, and then grab the info off it and store it into the variable.
//Dummy textField instance as a VC property.
let textField = UITextField()
//Add some setup to viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self //Don't forget to make vc conform to UITextFieldDelegateProtocol
textField.keyboardType = .phonePad
//http://stackoverflow.com/a/40640855/5153744 for setting up toolbar
let keyboardToolbar = UIToolbar()
keyboardToolbar.sizeToFit()
let flexBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneBarButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissKeyboard))
keyboardToolbar.items = [flexBarButton, doneBarButton]
textField.inputAccessoryView = keyboardToolbar
//You can't get the textField to become the first responder without adding it as a subview
//But don't worry because its frame is 0 so it won't show.
self.view.addSubview(textField)
}
//When done button is pressed this will get called and initate `textFieldDidEndEditing:`
func dismissKeyboard() {
view.endEditing(true)
}
//This is the whatever function you call when your timer is fired. Important thing is just line of code inside that our dummy code becomes first responder
func timerUp() {
textField.becomeFirstResponder()
}
//This is called when done is pressed and now you can grab value out of the textField and store it in any variable you want.
func textFieldDidEndEditing(_ textField: UITextField) {
textField.resignFirstResponder()
let intValue = Int(textField.text ?? "0") ?? 0
print(intValue)
}
I am using storyboard and this is what I did:
drag-drop a text field
On the storyboard, in the attributes inspector (having selected the text field), under "Drawing", select hidden
make an outlet for the text field in your view controller
make sure your view controller extends the UITextViewDelegate
make your current view controller the delegate
in the required location simply call <textfieldOutlet>.becomeFirstResponder()
Now that this is simply a textfield's data, u can always store the value and use it else where.

titleLabel.text vs currentTitle in Swift

I am trying to make a simple calculator with Swift. I want to get the "text" on the buttons I created. The instructor in the tutorial is using a property:
#IBAction func appendDigit(sender: UIButton) {
let digit = sender.currentTitle
}
The question is, if I did this:
#IBAction func appendDigit(sender: UIButton) {
let digit = sender.titleLabel.text
}
What's the difference? Will they yield the same results?
If so, how does one know when to use which?
titleLabel.text is mainly using to configure the text of the button(for each state)
currentTitle is read only. This is mainly using to get the title that is currently displayed.
You can't set this property because this set automatically whenever the button state changes. You can use currentTitle to get the title string associated with the button instead of using titleLabel.text because currentTitle property is set automatically whenever the button state changes.
Both .currentTitle and titleLabel.text return same value,
but .currentTitle is read-only, you can't modify his value (and it's only available in iOS 8+). .titleLabel is available since iOS 3+ and you can modify text, font, etc.
For sender.currentTitle:
The current title that is displayed on the button. (read-only)
Declaration
var currentTitle: String? { get }
Discussion
The value for this property is set automatically whenever the button
state changes. For states that do not have a custom title string
associated with them, this method returns the title that is currently
displayed, which is typically the one associated with the
UIControlStateNormal state. The value may be nil.
For sender.titleLabel.text:
A view that displays the value of the currentTitle property for a
button. (read-only)
Declaration
var titleLabel: UILabel? { get }
Discussion
Although this property is read-only, its own properties are
read/write. Use these properties primarily to configure the text of
the button. For example:
let button = UIButton.buttonWithType(.System) as UIButton
button.titleLabel.font = UIFont.systemFontOfSize(12)
button.titleLabel.lineBreakMode = .ByTruncatingTail
Do not use the label object to set the text color or the shadow color.
Instead, use the setTitleColor:forState: and
setTitleShadowColor:forState: methods of this class to make those
changes.
The titleLabel property returns a value even if the button has not
been displayed yet. The value of the property is nil for system
buttons.
From Apple Documentation.
In simple word:
sender.titleLabel?.text = "NewTitle" //this will work
sender.currentTitle = "newTitle" //this will give you an error because it is read only property.
And:
let digit = sender.currentTitle
let digit = sender.titleLabel!.text
This both will work because you are reading button's property and assigning it to digit instance.