I want to loop through my view subviews, and for each subview loop through its subviews and etc.
so let say I have the following code:
let view = myVC.view
view.backgroundColor = UIColor.clearColor()
then repeat this same for each subview. I want to do it functionally.
any insight is much appreciated.
EDIT:
to make it clear
I'm looking for something like this:
view.subviews.chanageColor() { (aView, aColor) in
aView.backgroundColor = aColor
}
but it should be recursive it goes to each view subview.
Something like this?
func makeAllSubviewsClear(view: UIView!)
{
view.backgroundColor = UIColor.clearColor()
for eachSubview in view.subviews
{
makeAllSubviewsClear(eachSubview)
}
}
called via:
let view = myVC.view
makeAllSubviewsClear(view)
Use a queue instead of recursing.
I think you could do something like this...
extension UIView {
func changeColor(color: UIColor) {
var queue = [self]
while let view = queue.first() {
queue += view.subviews
view.backgroundColor = color
queue.remove(view)
}
}
}
I'd do it with a queue instead of doing it recursively. That method isn't functional though.
Doing what your question wanted
You could do this way, which is what you put in your question as what you wanted to do.
extension UIView {
// this name isn't very good but you get the idea
func applyChangeToAllSubviews(block: (UIView) -> ()) {
var queue = [self]
// I don't have my dev computer dos not sure of the syntax
while let view = queue.first() {
queue += view.subviews
block(view)
queue.remove(view)
}
}
}
Then run it like...
someView.applyChangeToAllSubviews {
view in
view.backgroundColor = UIColor.whiteColor()
}
I guess that's a bit more functional... ish.
How about follwing:
let _ = myVC.view.subviews.map{ $0.backgroundColor = UIColor.clearColor() }
Although map should be used differently imho.
EDIT:
I've just looked that the OP wanted recursive loop which is different thing. You should move this code into function and call it recursively for each $0.
Something like this perhaps:
extension Array where Element: UIView {
func changeColor(color: UIColor) {
for view in self {
view.subviews.changeColor(color)
view.backgroundColor = color
}
}
}
Looks functional and can be called like this:
myVC.view.subviews.changeColor(color)
Inspired by #fogmaister one more solution:
extension Array where Element: UIView {
func applyChange(closure: (UIView)->()) {
for view in self {
view.subviews.applyChange(closure)
closure(view)
}
}
}
Here is a possible generic solution which allows to
traverse any "nested sequence". It is not restricted to UIView
and subviews, but takes a parameter which maps each sequence element to its immediate children.
It also separates the task of traversing the sequence from the
action on the elements (like setting a background color), by returning a Generator which
can be used with existing methods like forEach().
(This is inspired by some answers to Implementing recursive generator for simple tree structure in Swift.)
extension SequenceType {
public func generate<S : SequenceType where S.Generator.Element == Generator.Element>
(children children: Generator.Element -> S) -> AnyGenerator<Generator.Element> {
var selfGenerator = self.generate()
var childGenerator : AnyGenerator<Generator.Element>?
return anyGenerator {
// Enumerate all children of the current element:
if childGenerator != nil {
if let next = childGenerator!.next() {
return next
}
}
// Get next element of self, and prepare subGenerator to enumerate its children:
if let next = selfGenerator.next() {
childGenerator = children(next).generate(children: children)
return next
}
return nil
}
}
}
As an example, if view is a UIView then
view.subviews.generate(children: { $0.subviews })
returns a (lazy) generator of all (nested) subviews. By calling forEach() on this generator, we can modify each subview:
view.subviews.generate(children: { $0.subviews }).forEach {
$0.backgroundColor = UIColor.clearColor()
}
But it could also be used to get an array with all nested subviews:
let allViews = Array(view.subviews.generate(children: { $0.subviews }))
Related
I have a class like this:
class foo: UIView
{
static let childView: UIView
static func addView(onView: UIView)
{
//add childView onto onView
}
static func removeView(onView: UIView)
{
//remove childView from onView
}
}
I want to identify each onView separately, i.e.
If I do like this in a viewController
foo.addView(onView: self.view)
foo.addView(onView: self.view)
It should not remove childView only by calling removeView(onView: self.view) once, in-fact it should also call like this in order to remove child view
removeView(onView: self.view)
removeView(onView: self.view)
For this purpose, I need to identify each UIView separately.
How to identify each onView separately, and the number of times the child view add function is called for it ?
p.s I had tried this with accessibilityIdentifier and set a string tag when add view function is called, and increase the tag if onView has an associated tag with it. And in remove view, I am removing the child view, only if the tag value goes 0.
This somehow achieved what I want, but I am doubting on accessibilityIdentifier, will this disturb some built in functionality.
what apple says is: Here
But don't know what UI Automation Interfaces is, and what is accessibility label.
Edit: (Code inside functions)
static func addView(onView: UIView)
{
if let iden = onView.accessibilityIdentifier,
let identifier = Int(iden)
{
if identifier == 0
{
onView.accessibilityIdentifier = "1"
showHud(onView: onView)
}
else
{
onView.accessibilityIdentifier = "\(identifier + 1)"
}
}
else
{
onView.accessibilityIdentifier = "1"
showHud(onView: onView)
}
}
static func removeView(fromView: UIView)
{
if let iden = fromView.accessibilityIdentifier,
let identifier = Int(iden)
{
if identifier == 1
{
fromView.accessibilityIdentifier = "0"
childView.removeFromSuperview()
}
else
{
if identifier > 1
{
fromView.accessibilityIdentifier = "\(identifier - 1)"
}
}
}
else
{
fromView.accessibilityIdentifier = "0"
childView.removeFromSuperview()
}
}
I edited exactly what I want to achieve, this time different example without taken parameter in function.
What I want to achieve is:
var array = [String]()
func create() {
var arrayCount = array.count // will be 0
var nameForUI = "view/(arrayCount)" // will be view0
let nameForUI: UIVIew = {
let view = UIVIew()
return view
}()
array.append(nameForUI)
view.addSubview(nameForUI)
//
}
next time if I call create() func , the next view will be called "view1" So my question is, how to achieve this result? every time function will called it will create new element with new name.
Edit
To directly answer your question: No, you cannot do that.
Code written in Swift is compiled -- it is not an interpreted / scripted language.
So you cannot write code that creates a button named "littleButton" and then have a line of code littleButton.backgroundColor = .red
You can sort of do this by creating a Dictionary that maintains the "name" of the element as the key, and a reference to the element as the value.
Start with initializing an empty dictionary:
var buttonsDict: [String : UIButton] = [String : UIButton]()
Your "create" func can start like this:
func createButton(named str: String) -> Void {
// create a button
let b = UIButton()
// give it a default title
b.setTitle("Button", for: .normal)
// add it to our Dictionary
buttonsDict.updateValue(b, forKey: str)
}
When you want to create a button:
createButton(named: "littleButton")
When you want to access that button by name:
// use guard to make sure you get a valid button reference
guard let btn = buttonsDict["littleButton"] else { return }
view.addSubview(btn)
Edit 2
Another option, which is perhaps more similar to your edited question:
// initialize empty array of views
var viewsArray: [UIView] = [UIView]()
override func viewDidLoad() {
super.viewDidLoad()
// create 5 views
for _ in 0..<5 {
create()
}
// ...
// then, somewhere else in your code
viewsArray[0].backgroundColor = .red
viewsArray[1].backgroundColor = .green
viewsArray[2].backgroundColor = .blue
viewsArray[3].backgroundColor = .yellow
viewsArray[4].backgroundColor = .orange
}
func create() -> Void {
// create a view
let v = UIView()
// add it to our array
viewsArray.append(v)
// add it as a subview
view.addSubview(v)
}
As you see, instead of trying to reference the created views by name (which you cannot do), you can reference them by array index.
Just remember that arrays are zero-based... so the first element added to the array will be at [0] not [1].
The code does what I want it to do now, so I am asking this purely for learning something. Because I keep wondering if there's a more functional or Swift-y way to do it.
The goal is to figure out if UIView oneView is the parent of or a child of UIView anotherView, or that they shared a common ancestor. One thing I don't like is that I use var views: [UIView] = [view], isn't there a way to condense it to a list directly without initializing a var? Or any other improvements I might have missed?
private static func determineSharedNode(between oneView: UIView, and anotherView: UIView) throws -> UIView {
let oneViewStack = viewHierarchy(for: oneView)
let anotherViewStack = viewHierarchy(for: anotherView)
let sharedViews = oneViewStack.filter(anotherViewStack.contains)
guard let firstSharedView = sharedViews.first else {
throw ParentChildError.doNotShareNode
}
return firstSharedView
}
private static func viewHierarchy(for view: UIView) -> [UIView] {
var views: [UIView] = [view]
if let superview = view.superview {
views.append(contentsOf: viewHierarchy(for: superview))
}
return views
}
I have managed to condense it down to the following:
func viewHierarchy(for view: UIView) -> [UIView] {
return [view] + (view.superview.map(viewHierarchy) ?? [])
}
I think it's a lot better without intermediate var
Edited it a bit more so I got rid of the anonymous $0
In Swift 3 (XCode 8.3.3) I have a control in a UIStackView. I have an array of UIImageViews, and loop through the array to populate the stack view at run time:
for voiceIcon in voiceIcons {
let voiceView = UIImageView(image: voiceIcon)
addArrangedSubview(voiceView)
}
These icons will sometimes become disabled (replaced with a new image), so in order to update the control, I have a function to remove all the icons so that I can re-add the appropriate ones (if there's a better way, I'm listening!):
private func resetIconsView() {
for subUIView in self.subviews as [UIView] {
removeArrangedSubview(subUIView)
subUIView.removeFromSuperview()
print("Removing")
}
}
I've also tried
for subUIView in self.subviews as! [UIImageView] { ... }
I get the debug line "Removing" for each of the icons, but they still remain in the control and the UI. I'm new to Swift, so I'm likely not understanding something, what approach should I take?
Try code below:
for view in arrangedSubviews {
view.removeFromSuperview()
}
I am assuming your UIStackView only contains some UIImageView. You can iterate through all the arranged subviews of your stack view and update your image of that imageView. A sample implementation could look like below:
func changeImage() {
for view in self.arrangedSubviews {
if let imgView = view as? UIImageView {
imgView.image = UIImage(named: "taka_icon.png")
}
}
}
I did it with an extension. You have also to remove Constraints if existing. Else this can cause some trouble.
USAGE: myStackView.removeAllArrangedSubviews()
public extension UIStackView {
func removeAllArrangedSubviews() {
let removedSubviews = arrangedSubviews.reduce([]) { (allSubviews, subview) -> [UIView] in
self.removeArrangedSubview(subview)
return allSubviews + [subview]
}
// Deactivate all constraints
NSLayoutConstraint.deactivate(removedSubviews.flatMap({ $0.constraints }))
// Remove the views from self
removedSubviews.forEach({ $0.removeFromSuperview() })
}
}
I'm trying to find my UILabels in my superview of my UIViewControllers.
This is my code:
func watch(startTime:String, endTime:String) {
if superview == nil {println("NightWatcher: No viewcontroller specified");return}
listSubviewsOfView(self.superview!)
}
func listSubviewsOfView(view: UIView) {
var subviews = view.subviews
if (subviews.count == 0) { return }
view.backgroundColor = UIColor.blackColor()
for subview in subviews {
if subview.isKindOfClass(UILabel) {
// do something with label..
}
self.listSubviewsOfView(subview as UIView)
}
}
This is how it is recommended to in Objective-C, but in Swift I get nothing but UIViews and CALayer. I definitely have UILabels in the view that is supplied to this method. What am I missing?
The call in my UIViewController:
NightWatcher(view: self.view).watch("21:00", endTime: "08:30") // still working on
Here's a version that will return an Array of all the UILabel views in whatever view you pass in:
func getLabelsInView(view: UIView) -> [UILabel] {
var results = [UILabel]()
for subview in view.subviews as [UIView] {
if let labelView = subview as? UILabel {
results += [labelView]
} else {
results += getLabelsInView(view: subview)
}
}
return results
}
Then you can iterate over them to do whatever you'd like:
override func viewDidLoad() {
super.viewDidLoad()
let labels = getLabelsInView(self.view)
for label in labels {
println(label.text)
}
}
Using functional programming concepts you can achieve this much easier.
let labels = self.view.subviews.flatMap { $0 as? UILabel }
for label in labels {
//Do something with label
}
Swift 4
Adepting mKane's answer you can use this code:
let labels = self.view.subviews.compactMap { $0 as? UILabel }
for label in labels {
// do whatever
}
You could set a tag to your UILabel in the Storyboard or programmatically using:
myLabel.tag = 1234
Then, to find it use:
let myLabel = view.viewWithTag(1234)