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()
}
}
Related
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 have multiple controls on my screen. A collection view on right-top, then a button at left-center and besides the button, i have another collection view. Please refer the attached image for it
I am able to move the focus from button to bottom collection view and vice versa. I have created a focus guide for the same as below:
focusGuide.preferredFocusedView = self.btn
self.view.addLayoutGuide(self.focusGuide)
self.focusGuide.topAnchor.constraintEqualToAnchor(collectionViewHeader.topAnchor).active = true
self.focusGuide.bottomAnchor.constraintEqualToAnchor(collectionViewBottom.topAnchor).active = true
self.focusGuide.leadingAnchor.constraintEqualToAnchor(collectionViewBottom.leadingAnchor).active = true
self.focusGuide.widthAnchor.constraintEqualToAnchor(collectionViewBottom.widthAnchor).active = true
and in didUpdateFocusInContext: , I have write :
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
guard let nextFocusedView = context.nextFocusedView else { return }
if(nextFocusedView .isKindOfClass(bottomCell)) {
self.focusGuide.preferredFocusedView = self.btn
} else {
self.focusGuide.preferredFocusedView = self.collectionViewBottom
}
}
But, I am not able to move focus from button to top collection view. I may need multiple focus guide for this, but I do not know what should come there. Can anyone help me on this?
Thank you
I had similar problems in many parts of my UI so ended up defining a custom view to create larger areas of the screen that represent groups of focusable controls. Because I setup these larger areas to cover empty regions that don't contain controls, they intercept the focus movement and transfer to focus to the controls within the area independently of vertical or horizontal alignment with the original control where focus started from.
This is the custom view. You use it by placing one or more controls inside it in IB.
Note that it has additional features such as forcing focus to a specific control without having to override preferredFocusView but you can drop those if you don't need them).
class FocusGroup : UIView
{
weak private var nextFocusView:UIView? = nil
weak var initialView:UIView? = nil
var captureFocus:Bool = false
func focusOnView(view:UIView, now:Bool=false)
{
if not(view.isDescendantOfView(self))
|| view === self
{ return }
nextFocusView = view
setNeedsFocusUpdate()
if now { updateFocusIfNeeded() }
}
func resetFocus(now now:Bool=false)
{
nextFocusView = nil
setNeedsFocusUpdate()
if now { updateFocusIfNeeded() }
}
override func canBecomeFocused() -> Bool
{
if nextFocusView != nil { return true }
if containsFocus { return false }
return firstFocusableSubView() != nil
}
func firstFocusableSubView() -> UIView?
{
return findSubview({
$0.canBecomeFocused()
&& $0.userInteractionEnabled
&& $0.visible
&& ( not($0 is UIButton)
|| ($0 as! UIButton).enabled )
})
}
override var preferredFocusedView: UIView?
{
if let viewToFocus = ( nextFocusView ?? initialView ) ?? firstFocusableSubView()
{
return viewToFocus
}
return nil
}
override func shouldUpdateFocusInContext(context: UIFocusUpdateContext) -> Bool
{
// when capturing focus, prevent navigation outside of grouped subviews
if captureFocus
&& containsFocus
&& context.previouslyFocusedView!.isDescendantOfView(self)
&& (
context.nextFocusedView == nil
|| context.nextFocusedView === self
|| not(context.nextFocusedView!.isDescendantOfView(self))
)
{ return false }
return true
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)
{
// give focus to specific view as requested
if nextFocusView != nil
{
if context.nextFocusedView === nextFocusView
|| not(nextFocusView!.canBecomeFocused())
{ nextFocusView = nil }
return
}
}
}
Just insert a focus guide above the button and to the left of the top collection view, redirecting focus to the top collection view:
focusGuide.preferredFocusedView = self.topView
self.view.addLayoutGuide(focusGuide)
self.focusGuide.topAnchor.constraintEqualToAnchor(topView.topAnchor).active = true
self.focusGuide.bottomAnchor.constraintEqualToAnchor(topView.bottomAnchor).active = true
self.focusGuide.leadingAnchor.constraintEqualToAnchor(btn.leadingAnchor).active = true
self.focusGuide.trailingAnchor.constraintEqualToAnchor(btn.trailingAnchor).active = true
You may also want to insert a focus guide to the right of the button and below the top collection view so that navigating down from the top row redirects focus to the button instead of to the bottom row.
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 }))
I am trying to find the current focused First Responder by looping through the main UIView and all of it's SubViews, and all of it's SubViews through recursion, but I am coming up with nil.
extension UIView {
func getCurrentFirstResponder() -> AnyObject? {
if self.isFirstResponder() {
return self
}
for subView: UIView in self.subviews as [UIView] {
if subView.isFirstResponder() {
return subView
}
else {
subView.getCurrentFirstResponder()
}
}
return nil
}
}
let focusedView = self.view.getCurrentFirstResponder() as? UIView
Does this look correct? Why am I getting a nil view when I use this?
You code doesn't return anything in case the recursive call to subView.getCurrentFirstResponder() actually finds a first responder.
Try this:
for subView: UIView in self.subviews as [UIView] {
if subView.isFirstResponder() {
return subView
}
else {
if let sub = subView.getCurrentFirstResponder() {
return sub;
}
}
}
return nil
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)