Swift: get UI objects by ID? - swift

in iOS, is it possible to assign a string ID to UI objects and then retrieve them in the code by that ID?
I am looking for something similar to the Android's findViewById(id)

you can use viewWithTag but tag is type of Int:
let superView = UIView()
let subView = UIView()
subView.tag = 100
superView.addSubview(subView)
let v = superView.viewWithTag(100)
if use xib or storyboard you can bind id like this:
use runtime you can bind obj to obj ,but seems not you want :
objc_setAssociatedObject(superView, "key", subView, .OBJC_ASSOCIATION_RETAIN)
let v = objc_getAssociatedObject(superView, "key")
update:
you can use an enum to get the view :
enum UIKey:String {
case AA = "aa"
func findView(byKey:String ,fromView:UIView) -> UIView {
let v :UIView!
switch self {
// get view from real tag value
case .AA: v = fromView.viewWithTag(1)
}
return v
}
}
then use :
let dict = ["aa":123]
dict.forEach { (key,value) in
let v = UIKey(rawValue: key)?.findView(key, fromView: self.view)
//set v with your value
}

As you are having several viewControllers in the storyboard you're probably looking for UIStoryboards storyboard.instantiateViewControllerWithIdentifier(identifier: String)
// Basic example
let viewController = yourStoryboard.instantiateViewControllerWithIdentifier("id") as! UIViewController

I needed to share one UITableViewCell descendant with a UIButton across multiple UITableViewControllers. So IBOutlet was not an option for me and I needed something similar to Android findByViewId
So I did it in the UITableViewCell descendant when configure it:
for view in self.contentView.subviews {
if let button = view as? UIButton {
button.setTitle(item.label, for: .normal)
}
}
If you have more complicated cell layout I think you may use some specific view properties to identify the required subview.
P.S. from my experience using the Tag property for such purposes is generally not a good idea.

Related

What does UISearchBar.value for key do in 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.

Pass data to TabBarControllers after intialization?

How would I do something like this?
viewControllers!.forEach
{
$0.view
$0.m = self.m // error here
}
In each tabbarcontroller I defined m, yet this is not working.
I need it done through this as I initialize every tab by this.
Thanks.
The main problem is $0, the shorthand for the first parameter (i.e. a viewController in this case), is always immutable in a closure. There are a few other things to address to...
First of all you will need to subClass UIViewController to allow you to create/access the m property. At the most basic level this will be:
class MyVC: UIViewController {
var m: Int = 0 //giving a default value to save a .init in the example
}
The you will need to create MyVC view controllers, rather than standard UIViewControllers within your AppDelegate / SceneDelegate.
At which point you can adapt your original code to set the m property within each view controller:
if let count = tabBarController.viewControllers?.count {
for i in 0 ..< count {
if let vc = tabBarController.viewControllers?[i] as? MyVC {
vc.view // as per the original, but can't see any point in it
vc.m = m
}
}
}

ScrollView not reloading upon dismissing ViewController and calling viewDidLoad

I have a scrollView that contains a dynamic amount of WeatherViewControllers each displaying the weather data of a different city the user has saved. The user can segue from the WeatherViewControllers to a CityListViewController. Where they can add and remove cities from their list which in turn should add and remove WeatherViewControllers from the scrollView upon dismissing the CityListViewController, this is where I am running into a problem.
Currently I am using a protocol to call viewDidLoad in the scrollViewController upon dismissing the CityListViewController which works as I follow the code with the debugger all of the code that should be getting called is and the variable which tracks how many viewControllers to create is accurate after the change, however the scrollView is not changing. If I add/remove a city from the list the scrollView still displays the same amount as before.
The func createAndAddWeatherScreen is called the accurate amount of times after the change and everything and if I close the app and reopen it the scrollView then displays the right amount of viewControllers. It seems like everything is working except the scrollView is not reloading upon dismissing the cityListController.
Side Note: Upon initially opening the app the scrollView loads properly with all the correct WeatherViewControllers in the UIScrollView and the correct cities in the list.
class ScrollViewController: UIViewController, ScrollReloadProtocol {
func reloadScrollView() {
self.viewDidLoad()
}
//#IBOutlet var totalScrollView: UIScrollView!
var pages = [ViewController]()
var x = 0
var weatherScreensArray = [SavedCityEntity]()
var weatherScreenStringArray = [String]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var horizString = "H:|[page1(==view)]"
let defaults = UserDefaults.standard
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.isPagingEnabled = true
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
view = scrollView
//userDefaults used to keep track of which screen is which to put different cities on different viewControllers
defaults.set(0, forKey: "screenNumber")
//load cities to get number of cities saved
loadCities()
var views : [String: UIView] = ["view": view]
//create all weatherWeatherControllers
while x <= weatherScreensArray.count {
pages.append(createAndAddWeatherScreen(number: x))
weatherScreenStringArray.append("page\(x+1)")
views["\(weatherScreenStringArray[x])"] = pages[x].view
let addToHoriz = "[\(weatherScreenStringArray[x])(==view)]"
horizString.append(addToHoriz)
x+=1
}
horizString.append("|")
let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[page1(==view)]|", options: [], metrics: nil, views: views)
let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: horizString, options: [.alignAllTop, .alignAllBottom], metrics: nil, views: views)
NSLayoutConstraint.activate(verticalConstraints + horizontalConstraints)
}
//Function to create and add weatherViewController
func createAndAddWeatherScreen(number: Int) -> ViewController {
defaults.set(number, forKey: "screenNumber")
let story = UIStoryboard(name: "Main", bundle: nil)
let weatherScreen = story.instantiateViewController(identifier: "View Controller") as! ViewController
weatherScreen.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(weatherScreen.view)
addChild(weatherScreen)
weatherScreen.didMove(toParent: self)
return weatherScreen
}
There are several architectural issues with your code:
You should not be calling the viewDidLoad method manually, it's very easy to extract the necessary code for fetching and reloading into a method of its own which doesn't break the lifecycle of a viewController.
It looks like you're attempting to use the scrollView as a tableView which offers reloading as a free function, as well as far better memory management.
You could use .map() instead of using while to iterate and create new arrays manually, higher order functions are very useful.
Despite all that, you can't actually reload a scrollView in the way you can a tableView. It already contains the views you added there previously, and it looks to me like you're stacking extra views over and over again, you could debug if this is the case with the visual debugger.
Lastly, in order to update a view after updating constraints you need to call:
setNeedsLayout()
layoutIfNeeded()
to let the system know you want to update the subviews and their layout.

find descendant buttons in nested container

I have placed buttons in multiple nested StackViews
I then placed the views within a CollectionView (yes I recognize this isn't necessarily the best or optimal way to do this).
I'm trying to figure out how I can programmatically find all my buttons.
If I do this, all I get is my StackView instead of getting the buttons (which are now 3rd level descendants). I'd love to not have to right a nested loop and instead find a function that helps me find all descendants of type UIButton.
for subview in buttonContainer.subviews {
let button = subview as? UIButton
if button != nil {
button!.setTitle("TEST", for: UIControl.State.normal)
}
}
The reason I'm doing this is just as an exercise to learn different methods, which is why I am ok nesting multiple stack views and then placing them within a CollectionView
You need to create a recursive method like:
func buttonsIn(_ view: UIView) {
if let button = view as? UIButton {
button.setTitle("TEST", for: .normal)
} else {
view.subviews.forEach({ buttonsIn($0) })
}
}
Usage:
buttonsIn(view)
Modify the recursive method as per your requirement.
I didn't test it but using recursion you should be able to accomplish this. Something like this may work for you:
func findButtonsIn(_ view: UIView) -> [UIButton] {
var buttons: [UIButton] = []
view.subviews.forEach({
if let button = $0 as? UIButton {
buttons.append(button)
} else {
buttons.append(contentsOf: self.findButtonsIn($0))
}
})
return buttons
}
Usage
findButtonsIn(yourView)
The function will return all found UIButtons as an array.
You could use
self.findButtonsIn(UIView()).enumerated().forEach({
$0.element.setTitle("\($0.offset)", for: .normal)
})
for example to set the buttons title according to the order they were found in.

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.