0

I have a tabBar with 5 items. To get the frame of the first tabBar item and set something based on that frame I use this which works perfectly:

guard let firstTab = tabBarController?.tabBar.items?[0].value(forKey: "view") as? UIView else { return }
        
guard let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first else { return }

let windowRect = lastTab.convert(firstTab.frame, to: window)

print(firstTab) // (2.0, 1.0, 79.00000000298023, 48.0)
print(windowRect) // (4.0, 689.0, 79.00000000298023, 48.0)

But when I try the above code to get the frame of the 5th tab, the values are incorrect. When setting tabBar.items?[4], the x coordinate of the frame is way off screen at 665 in the windowRect:

guard let lastTab = tabBarControlle?.tabBar.items?[4].value(forKey: "view") as? UIView else { return }

guard let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first else { return }

let windowRect = lastTab.convert(lastTab.frame, to: window)

print(lastTab) // (332.99999999701976, 1.0, 79.00000000298023, 48.0)
print(windowRect) // (665.9999999940395, 689.0, 79.0000000029803, 48.0)

But setting tabBar.items?[2] I get the correct x coordinate at 336 in the windowRect:

// setting the 2nd item gives me the correct frame for the 4th item
guard let lastTab = tabBarControlle?.tabBar.items?[2].value(forKey: "view") as? UIView else { return }

guard let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first else { return }

let windowRect = lastTab.convert(lastTab.frame, to: window)

print(lastTab) // (168.00000000596046, 1.0, 77.99999998807907, 48.0)
print(windowRect) // (336.0000000119209, 689.0, 77.99999998807908, 48.0)

Why is tabBar.items?[2] returning the value for tabBar.items?[4] in the window’s frame?

Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • not related to your question but why not simply `windows.first(where: \.isKeyWindow)` ? – Leo Dabus Dec 18 '20 at 23:47
  • When the warning started to appear in XCode 12/13 I started using `.filter({..})` because it was the first answer that I found that worked and I just never switched over. I saw `\.` on another answer but by then all my code was using `.filter`. – Lance Samaria Dec 18 '20 at 23:54
  • `filter` will unnecessarily iterate the whole collection. In this case it doesn't make a difference because there is just a few windows. Make sure to always use `first(where:)` that it will provide an early exit when the condition is met. – Leo Dabus Dec 19 '20 at 00:03
  • 1
    ohhhhhhh, I never knew, learn something new everyday. Thanks for the advice. Now I’m going to switch them all over . If I’m going to add a the clause I might as well use the shorter syntax – Lance Samaria Dec 19 '20 at 00:04
  • @LeoDabus how can I use `filter` and `where` example `let arr = view.subviews.filter({ $0 is CustomLabel })`; `arr.forEach({ $0.removeFromSuperview() })`? I looked at my code and thought about what you said. Doing this will remove all of the CustomLabels but I have to iterate through every subview inside the the vc's view which has a lot of subviews – Lance Samaria Jan 01 '21 at 10:09
  • Do you have more that one `CustomLabel` in your subviews? If there is only one custom label `view.subviews.first(where: { $0 is CustomLabel })?.removeFromSuperview() ` – Leo Dabus Jan 01 '21 at 14:47
  • @LeoDabus I have more then one that I want removed – Lance Samaria Jan 01 '21 at 16:19
  • So first where doesn't make any sense. You would need to iterate the whole collection anyway. Your arr already have only the items you want to remove so your code looks fine. Another option is to use a regular for loop with a where condition and remove them on the fly, – Leo Dabus Jan 01 '21 at 16:21
  • I thought using `where` would prevent me from having to iterate through every item. Correct if I’m wrong, but now that I’m looking at it, `where` is similar to `break`. Once it finds what it’s looking for, does what it does, it exits the loop? – Lance Samaria Jan 01 '21 at 16:26
  • No it depends on which method we are talking about. First and contains naturally provides an early exit when a condition is met. There is no way to avoid iterating the whole collection if you are looking for multiple occurrences unless you know exactly how many you are looking for. – Leo Dabus Jan 01 '21 at 17:05
  • How can you do it if you know the exact amount? For example say there are 3 occurrences? I never knew that was possible. I just iterate the whole collection – Lance Samaria Jan 01 '21 at 17:14
  • Use the for loop and add a counter. You can put the counter condition as well in your where clause `for view in subviews where predicate && counter < 3 {` – Leo Dabus Jan 01 '21 at 17:16
  • 1
    Oh I get it, on every occurrence the count increases and once it hits whatever max you break. I thought you had another way. I’ve used that before. I’ve never used it with a where clause though. – Lance Samaria Jan 01 '21 at 17:18
  • Actually you shouldn’t use it there. Add an if condition and a break – Leo Dabus Jan 01 '21 at 17:20
  • It will not break the loop without calling break inside the loop – Leo Dabus Jan 01 '21 at 17:21
  • Just add an if condition inside the loop and break it. – Leo Dabus Jan 01 '21 at 17:22
  • Oh you’re talking about with the where clause? – Lance Samaria Jan 01 '21 at 17:23
  • Yes forget about adding the counter condition to the where clause. My bad. – Leo Dabus Jan 01 '21 at 17:24
  • Oh ok, I got you. Hey man thanks for your help and have a Happy and safe New Year!!! – Lance Samaria Jan 01 '21 at 17:30
  • You are welcome. Happy New Year for you too !!! Take care. – Leo Dabus Jan 01 '21 at 17:31

1 Answers1

0

You missed .superview.

private func convertTabFrame(for index: Int) -> CGRect? {
    guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return nil }
    guard let tabView = self.tabBarController?.tabBar.items?[index].value(forKey: "view") as? UIView else { return nil }
    return tabView.superview?.convert(tabView.frame, to: window)
}
Raja Kishan
  • 16,767
  • 2
  • 26
  • 52