How to set text in GMSAutocompleteViewController in Google Place AutocompleteViewController - swift

I need help to set text in searchBar of GMSAutocompleteViewController when open in my App. I am using GMSAutocompleteViewController in Google Place AutocompleteViewController.

I got a working solution by combining #Youngjin and #Exception's answers for Swift 4 and Google Places 2.6.0
To access the GMSAutocompleteViewController searchbar:
let views = gmsAutoCompleteViewController.view.subviews
let subviewsOfSubview = views.first!.subviews
let subOfNavTransitionView = subviewsOfSubview[1].subviews
let subOfContentView = subOfNavTransitionView[2].subviews
let searchBar = subOfContentView[0] as! UISearchBar
Then to set the text and also search automatically:
searchBar.text = "Your address"
searchBar.delegate?.searchBar?(searchBar, textDidChange: "Your address") // This performs the automatic searching.
I found that I received a EXC_BAD_ACCESS error when attempting to set the text from within
didRequestAutocompletePredictions(_ viewController: GMSAutocompleteViewController).
So I instead put this code in the completion block where I present the autoCompleteController, which works without issue.
Combining it all together:
let gmsAutoCompleteViewController = GMSAutocompleteViewController()
gmsAutoCompleteViewController.delegate = self
present(gmsAutoCompleteViewController, animated: true) {
let views = gmsAutoCompleteViewController.view.subviews
let subviewsOfSubview = views.first!.subviews
let subOfNavTransitionView = subviewsOfSubview[1].subviews
let subOfContentView = subOfNavTransitionView[2].subviews
let searchBar = subOfContentView[0] as! UISearchBar
searchBar.text = "Your address"
searchBar.delegate?.searchBar?(searchBar, textDidChange: "Your address")
}
EDIT: I found that this solution only seems to work on iOS 11.
let searchBar = subOfContentView[0] as! UISearchBar
will fail on iOS 10, and possibly lower versions.

#Cools
In Swift 3, it seems to have different hierarchy. I've digged into VC and got the place.
func didRequestAutocompletePredictions(_ viewController: GMSAutocompleteViewController) {
let views = viewController.view.subviews
let subviewsOfSubview = views.first!.subviews
let subOfNavTransitionView = subviewsOfSubview[1].subviews
let subOfContentView = subOfNavTransitionView[2].subviews
let searchBar = subOfContentView[0] as! UISearchBar
searchBar.text = "YOUR_TEXT"
}
However, it has one fault that it does not search automatically. After I'v e got the solution, I will edit again.

Well, I don't necessarily recommend this course of action because if you want more control you should use the UISearchResults Controller. However, here's a swifty-er version:
//Where viewController is the GMSAutocompleteViewController
if let searchBar = (viewController.view.subviews
.flatMap { $0.subviews }
.flatMap { $0.subviews }
.flatMap { $0.subviews }
.filter { $0 == $0 as? UISearchBar}).first as? UISearchBar {
searchBar.text = "YOUR_TEXT_HERE"
searchBar.delegate?.searchBar?(searchBar, textDidChange: "YOUR_TEXT_HERE") // to get the autoComplete Response
}
The benefit of this approach is that you don't have to know exactly where the SearchBar is located. It's basically trying to flatten all the subviews into one array and filter out the SearchBar from the array. If Google decides to change up where the SearchBar is hidden, this approach may still find it. The problem is that you still need to know how many levels of subviews there are. There may be a recursive function that you could set up to deal with this.

I have found out that solutions here are not working for iOS 10. So I have written recursive method for finding concrete view - UISearchBar, in our case:
extension UIView {
func getViewWithConcreteType<T: UIView>()-> T? {
if let concreteSubview = self as? T {
return concreteSubview
}
for subview in subviews {
let concreteView: T? = subview.getViewWithConcreteType()
if concreteView != nil {
return concreteView!
}
}
return nil
}
}
Made some modification to original solution:
let views = autocompleteController.view.subviews
let subviewsOfSubview = views.first!.subviews
let subOfNavTransitionView = subviewsOfSubview[1].subviews
let subOfContentView = subOfNavTransitionView[2].subviews
if let searchBar = subOfContentView[0] as? UISearchBar {
searchBar.text = text
searchBar.delegate?.searchBar?(searchBar, textDidChange: text)
} else {
let searchBar: UISearchBar? = autocompleteController.view.getViewWithConcreteType()
searchBar?.text = text
searchBar.map { $0.delegate?.searchBar?($0, textDidChange: text) }
}

You can inject text in this search bar using something like
func injectTextInSearchView(_ text: String, _ view: UIView) {
if let view = view as? UISearchBar {
view.text = text
view.delegate?.searchBar!(view, textDidChange: text)
return
}
for subview in view.subviews {
injectTextInSearchView(text, subview)
}
}

- (void)viewDidLoad {
[super viewDidLoad];
acController = [[GMSAutocompleteViewController alloc] init];
acController.delegate = self;
}
- (IBAction)btnSearch1:(id)sender {
[self presentViewController:acController animated:YES completion:nil];
}
- (void)didRequestAutocompletePredictions:(GMSAutocompleteViewController *)viewController {
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
UIView *Views = viewController.view;
NSArray *Aar2 = [[Views subviews][0] subviews];
UIView *View2 = Aar2[1];
UIView *View3 = [View2 subviews][2];
UISearchBar *Search = [[View3 subviews] firstObject];
Search.text = #"Your text";
[Search.delegate searchBar:Search textDidChange:txtAddress.text];
}
// Cools enjoy man

I'm not very familiar with swift coding but based from the documentation - Customize text and background colors:
You can set the colors of all text and backgrounds in the autocomplete UI control, to make the widget match the visual appearance of your app more closely. There are two ways to set the UI control colors:
By using the native iOS UIAppearance protocol to globally style UI controls where possible. These settings apply to many, but not all, of the UI control elements.
By using the SDK methods on the widget classes to set properties which are not supported by the UIAppearance protocol.
The documentation states Search Bar text color, Search bar tint color and Search bar placeholder text color (default search text) can be styled using (UIAppearance protocol or SDK method).
Hope this info helps.

typedef void (^TTSearchBarHandler)(UISearchBar * searchbar);
__weak UIViewController *weaks = self;
[self searchSubviews:viewController.view completion:^(UISearchBar *searchbar) {
if(searchbar){
[searchbar.delegate searchBar:searchbar textDidChange:#"Your Text"];
}
}];
-(void)searchSubviews:(UIView*)view completion:(TTSearchBarHandler)completion{
if([view isKindOfClass:[UISearchBar class]]){
UISearchBar *searchBar = (UISearchBar*)view;
if(completion){
completion(searchBar);
}
}
else{
for(UIView * subview in view.subviews){
[self searchSubviews:subview completion:^(UISearchBar *searchbar) {
if(completion){
completion(searchbar);
}
}];
}
}
}

Related

Trying to hide SearchBar through Alpha in Swift

I'm using Xcode 9 and Swift 4. I'm having trouble trying to use the scroll in y to hide SearchBar through alpha. How to access this background to try to hide it?
code:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let transp:Float = Float(1 - (self.mTableView.contentOffset.y / (self.mTableView.contentSize.height - self.mTableView.frame.size.height) * 60))
self.navigationController!.navigationBar.alpha = CGFloat(transp)
self.mSearchController.searchBar.alpha = CGFloat(transp)
self.mSearchController.searchBar.isTranslucent = true
titleMsgView.alpha = CGFloat(transp)
}
Here's a sample gif link:
https://www.dropbox.com/s/6is5mukpk1yew8e/bgSearch.gif?dl=0
The SearchView is pretty tough when it comes to changing how it looks. You could try looping over the subviews of the SearchView and setting all their alpha values to 0, like this:
for subView in mSearchController.searchBar.subviews {
subView.alpha = 0
for deepSubView in subView.subviews {
deepSubView.alpha = 0
}
}
Also, a quick note: you can remove a lot of the self keywords from your code.
EDIT: That just removed the searchbar, but with some editing we can fix that:
for subview in mSearchController.searchBar.subviews.last!.subviews {
if type(of: subview) == NSClassFromString("UISearchBarBackground") {
subview.alpha = 0
}
}
That should do the trick!

NSViewController slow to register mouse events?

I've been working on a small image uploading menubar app for OS X. I've created custom NSView subclass for the uploaded items.
Here's what it looks like by default:
Mouse events are handled by the view's NSViewController in the following way:
import Cocoa
class MenuItemController: NSViewController {
private var trackingArea: NSTrackingArea?
override func mouseEntered(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
v.shouldHighlight = true
v.needsDisplay = true
}
}
override func mouseExited(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
v.shouldHighlight = false
v.needsDisplay = true
}
}
override func viewDidLoad() {
super.viewDidLoad()
if (trackingArea == nil) {
trackingArea = NSTrackingArea(rect: self.view.bounds, options: [.ActiveAlways, .MouseEnteredAndExited], owner: self, userInfo: nil)
self.view.addTrackingArea(trackingArea!)
}
/* rest of the code... */
}
}
It works fine until I move my cursor fast over the items. It seems like the mouseExited() event is not called, and the view remains with a blue background (mouse is actually on the Quit button):
I also tried moving the mouse handling into the NSView, but with same results. I appreciate any input! Thanks!
In my opinion, Apple has had bugs in this area.
Assuming you update your tracking area according to apple docs,
adding this additional fix might fix your problem... it fixes it for me in many cases.
I verify in mouseMoved / mouseEntered routine that the mouse cursor is still within my views frame, and if not, call mouseExited: myself.
- (void) adjustTrackingArea
{
if ( trackingArea )
{
[self removeTrackingArea:trackingArea];
[trackingArea release];
}
// determine the tracking options
NSTrackingAreaOptions trackingOptions = // NSTrackingEnabledDuringMouseDrag | // don't track during drag
NSTrackingMouseMoved |
NSTrackingMouseEnteredAndExited |
//NSTrackingActiveInActiveApp | NSTrackingActiveInKeyWindow | NSTrackingActiveWhenFirstResponder |
NSTrackingActiveAlways;
NSRect theRect = [self visibleRect];
trackingArea = [[NSTrackingArea alloc]
initWithRect: theRect
options: trackingOptions
owner: self
userInfo: nil];
[self addTrackingArea:trackingArea];
}
- (void)resetCursorRects
{
[self adjustTrackingArea];
}
- (void)mouseEntered:(NSEvent *)ev
{
[self setNeedsDisplay:YES];
// make sure current mouse cursor location remains under the mouse cursor
NSPoint cursorPt = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:NULL];
// apple bug!!!
//NSPoint cursorPt2 = [self convertPointFromBase:[ev locationInWindow]];
//if ( cursorPt.x != cursorPt2.x )
// NSLog( #"hello old cursorPt" );
NSRect r = [self frame];
if ( cursorPt.x > NSMaxX( r ) || cursorPt.x < 0 )
{
[self mouseExited:ev];
//cursorPt.x = [self convertPointFromBase:[ev locationInWindow]];
//if ( cursorPt.x > NSMaxX( r ) || cursorPt.x < r.origin.x )
return;
}
... your custom stuff here ...
}
- (void)mouseExited:(NSEvent *)theEvent
{
if ( isTrackingCursor == NO )
return;
[[NSCursor arrowCursor] set];
isTrackingCursor = NO;
[self setNeedsDisplay:YES];
}
- (void)mouseMoved:(NSEvent *)theEvent
{
[self mouseEntered:theEvent];
}
I do not understand whether you are installing an NSTrackingArea for the whole window (in this case menu) or for each item. If you are doing the latter, don't do it, or you will spend an endless time to correct the problems you are seeing. The way I handled this buggy behaviour is to create an NSTrackingArea for the whole window and I figure myself where the mouse is and handle the highlighting of each item myself. I know this is not ideal, but this was the only way I was able to solve it after knocking myself in the head for three days.
You can track which was the old active menuitem and set manage it accordingly, something like:
override func mouseEntered(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
lastEntered.shouldHighLight = false
lastEntered.needsDisplay = true
lastEntered = v;
lastEntered.shouldHighLight = true
lastEntered.needsDisplay = true
}
}
Thus you will ensure that at most one will be active.

swift- Assigning the value to the custom button from subview

Hi I'm trying to assign the particular subview in custom buttons in for loop it is working fine in objective c, but I'm facing problem in swift code.
objective c code:
HButton *view = nil; // hbutton class object
NSArray *subviews = [self.superview subviews]; // storing the subviews in subviews array
NSInteger i = 0; // for index
for (view in subviews)
{
if ([view isKindOfClass:[HButton class]])
{
HButton *check=[subviews objectAtIndex:i];
if (self.tag == check.tag)
{
self.titleLabel.text =#"hel";
self.backgroundColor =[UIColor orangeColor];
}
i++;
}
Swift Code:
var view1 :BTNCustom! = nil
var subviews : NSArray = self.superview.subviews
var ival : Int = 0;
for view1 : AnyObject in subviews // I'm getting the error in this line
{
if view1.isKindOfClass(BTNCustom)
{
var check : BTNCustom = subviews.objectAtIndex(ival) as BTNCustom
if (self.tag == check.tag)
{
self.titleLabel.text = "hel";
self.backgroundColor = UIColor.orangeColor()
}
ival++;
}
}
Can you be more clear what you are actually trying to do? The following is the closest thing I can imagine to what you are trying to do:
for view in superview.subviews {
if view.isKindOfClass(BTNCustom) && tag == view.tag {
titleLabel.text = "hel"
backgroundColor = UIColor.orangeColor()
}
}
However, it really looks like you are abusing tags and not really understanding the point of a type system.
Specific problems with your code:
1) var view1 :BTNCustom! = nil you are assigning nil to something that you are saying you want to be automatically unwrapped, why not just var view1: BTNCustom!, they should be the same but the latter is much more palatable.
2) You are trying to use view1 as your loop variable of a different type when you have already declared it to be BTNCustom. Why? Are you really using variable names like this? Do you have a limit on the number of characters you are allowed to use?
3) What is the point of using an extra integer to pull views out of the subview array when in fact that view is exactly the one you are already have a reference to? To be clear, in your Obj-C code, both view and check will always be identical whenever you actually use check.
Just trying to solved your error may this will help you!
for index in 1...subviews
{
let view1:AnyObject = subViews[index] as AnyObject
if view1.isKindOfClass(BTNCustom)
{
var check : BTNCustom = subviews.objectAtIndex(ival) as BTNCustom
if (self.tag == check.tag)
{
self.titleLabel.text = "hel";
self.backgroundColor = UIColor.orangeColor()
}
ival++;
}
}

Is it possible to change UITabBarItem badge color

I want to change background color of UITabBarItem badge but can't find any resource on how to make it.
UITabBarItem has this available since iOS 10.
var badgeColor: UIColor? { get set }
It's also available via appearence.
if #available(iOS 10, *) {
UITabBarItem.appearance().badgeColor = .green
}
reference docs:
https://developer.apple.com/reference/uikit/uitabbaritem/1648567-badgecolor
Changing the badge-color is now natively supported in iOS 10 and later using the badgeColor property inside your UITabBarItem. See the apple docs for more infos on the property.
Example:
Swift 3: myTab.badgeColor = UIColor.blue
Objective-C: [myTab setBadgeColor:[UIColor blueColor]];
I wrote this piece of code for my app, but I have only tested it in iOS 7.
for (UIView* tabBarButton in self.tabBar.subviews) {
for (UIView* badgeView in tabBarButton.subviews) {
NSString* className = NSStringFromClass([badgeView class]);
// looking for _UIBadgeView
if ([className rangeOfString:#"BadgeView"].location != NSNotFound) {
for (UIView* badgeSubview in badgeView.subviews) {
NSString* className = NSStringFromClass([badgeSubview class]);
// looking for _UIBadgeBackground
if ([className rangeOfString:#"BadgeBackground"].location != NSNotFound) {
#try {
[badgeSubview setValue:[UIImage imageNamed:#"YourCustomImage.png"] forKey:#"image"];
}
#catch (NSException *exception) {}
}
if ([badgeSubview isKindOfClass:[UILabel class]]) {
((UILabel *)badgeSubview).textColor = [UIColor greenColor];
}
}
}
}
}
You're only able to update the badge background with an image, not a color. I have also exposed the badge label if you wanted to update that in some way.
Its important to note that this code must be called after setting the tabBarItem.badgeValue!
EDIT: 4/14/14
The above code will work in iOS 7 when called anywhere. To get it working in iOS 7.1 call it in the view controllers -viewWillLayoutSubviews.
EDIT: 12/22/14
Here's an updated snippet which I'm currently using. I put the code in a category extension for simplicity.
- (void)badgeViews:(void (^)(UIView* badgeView, UILabel* badgeLabel, UIView* badgeBackground))block {
if (block) {
for (UIView* tabBarButton in self.subviews) {
for (UIView* badgeView in tabBarButton.subviews) {
NSString* className = NSStringFromClass([badgeView class]);
if ([className rangeOfString:#"BadgeView"].location != NSNotFound) {
UILabel* badgeLabel;
UIView* badgeBackground;
for (UIView* badgeSubview in badgeView.subviews) {
NSString* className = NSStringFromClass([badgeSubview class]);
if ([badgeSubview isKindOfClass:[UILabel class]]) {
badgeLabel = (UILabel *)badgeSubview;
} else if ([className rangeOfString:#"BadgeBackground"].location != NSNotFound) {
badgeBackground = badgeSubview;
}
}
block(badgeView, badgeLabel, badgeBackground);
}
}
}
}
}
Then when you're ready to call it, it'll look like this.
[self.tabBar badgeViews:^(UIView *badgeView, UILabel *badgeLabel, UIView *badgeBackground) {
}];
EDIT: 11/16/15
It's been brought to my attention that some people need a little more clarity on what's happening in this code. The for loops are searching for a few views which are not publicly accessible. By checking if the views class name contains a part of the expected name, it's ensuring to reach the intended view while not setting off any possible red flags by Apple. Once everything has been located, a block is executed with easy access to these views.
It's noteworthy that the possibility exists for this code to stop working in a future iOS update. For example these internal views could one day acquire different class names. However the chances of that are next to none since even internally Apple rarely refactors classes to this nature. But even if they were to, it would be something along the title of UITabBarBadgeView, which would still reach the expected point in code. Being that iOS9 is well out the door and this code is still working as intended, you can expect this problem to never arise.
I have the same problem and solved it by creating a little category that replace the BadgeView with an UILabel that you can customize easily.
https://github.com/enryold/UITabBarItem-CustomBadge/
For people using Swift, I managed to improve on TimWhiting answer in order to have the badge view working on any screen size and any orientation.
extension UITabBarController {
func setBadges(badgeValues: [Int]) {
for view in self.tabBar.subviews {
if view is CustomTabBadge {
view.removeFromSuperview()
}
}
for index in 0...badgeValues.count-1 {
if badgeValues[index] != 0 {
addBadge(index, value: badgeValues[index], color:UIColor(paletteItem: .Accent), font: UIFont(name: Constants.ThemeApp.regularFontName, size: 11)!)
}
}
}
func addBadge(index: Int, value: Int, color: UIColor, font: UIFont) {
let badgeView = CustomTabBadge()
badgeView.clipsToBounds = true
badgeView.textColor = UIColor.whiteColor()
badgeView.textAlignment = .Center
badgeView.font = font
badgeView.text = String(value)
badgeView.backgroundColor = color
badgeView.tag = index
tabBar.addSubview(badgeView)
self.positionBadges()
}
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.positionBadges()
}
// Positioning
func positionBadges() {
var tabbarButtons = self.tabBar.subviews.filter { (view: UIView) -> Bool in
return view.userInteractionEnabled // only UITabBarButton are userInteractionEnabled
}
tabbarButtons = tabbarButtons.sort({ $0.frame.origin.x < $1.frame.origin.x })
for view in self.tabBar.subviews {
if view is CustomTabBadge {
let badgeView = view as! CustomTabBadge
self.positionBadge(badgeView, items:tabbarButtons, index: badgeView.tag)
}
}
}
func positionBadge(badgeView: UIView, items: [UIView], index: Int) {
let itemView = items[index]
let center = itemView.center
let xOffset: CGFloat = 12
let yOffset: CGFloat = -14
badgeView.frame.size = CGSizeMake(17, 17)
badgeView.center = CGPointMake(center.x + xOffset, center.y + yOffset)
badgeView.layer.cornerRadius = badgeView.bounds.width/2
tabBar.bringSubviewToFront(badgeView)
}
}
class CustomTabBadge: UILabel {}
No you can't change the color but you can use your own badges instead. Add this extension at the file scope and you can customise the badges however you like. Just call self.tabBarController!.setBadges([1,0,2]) in any of your root view controllers.
To be clear that is for a tab bar with three items, with the badge values going from left to right.
extension UITabBarController {
func setBadges(badgeValues:[Int]){
var labelExistsForIndex = [Bool]()
for value in badgeValues {
labelExistsForIndex.append(false)
}
for view in self.tabBar.subviews {
if view.isKindOfClass(PGTabBadge) {
let badgeView = view as! PGTabBadge
let index = badgeView.tag
if badgeValues[index]==0 {
badgeView.removeFromSuperview()
}
labelExistsForIndex[index]=true
badgeView.text = String(badgeValues[index])
}
}
for var i=0;i<labelExistsForIndex.count;i++ {
if labelExistsForIndex[i] == false {
if badgeValues[i] > 0 {
addBadge(i, value: badgeValues[i], color:UIColor(red: 4/255, green: 110/255, blue: 188/255, alpha: 1), font: UIFont(name: "Helvetica-Light", size: 11)!)
}
}
}
}
func addBadge(index:Int,value:Int, color:UIColor, font:UIFont){
let itemPosition = CGFloat(index+1)
let itemWidth:CGFloat = tabBar.frame.width / CGFloat(tabBar.items!.count)
let bgColor = color
let xOffset:CGFloat = 12
let yOffset:CGFloat = -9
var badgeView = PGTabBadge()
badgeView.frame.size=CGSizeMake(17, 17)
badgeView.center=CGPointMake((itemWidth * itemPosition)-(itemWidth/2)+xOffset, 20+yOffset)
badgeView.layer.cornerRadius=badgeView.bounds.width/2
badgeView.clipsToBounds=true
badgeView.textColor=UIColor.whiteColor()
badgeView.textAlignment = .Center
badgeView.font = font
badgeView.text = String(value)
badgeView.backgroundColor = bgColor
badgeView.tag=index
tabBar.addSubview(badgeView)
}
}
class PGTabBadge: UILabel {
}
Swift 3 Here is an updated version of #Kirualex's answer (who improved on #TimWhiting's answer) for Swift 3.
extension UITabBarController {
func setBadges(badgeValues: [Int]) {
for view in self.tabBar.subviews {
if view is CustomTabBadge {
view.removeFromSuperview()
}
}
for index in 0...badgeValues.count-1 {
if badgeValues[index] != 0 {
addBadge(index: index, value: badgeValues[index], color: UIColor.blue, font: UIFont(name: "Helvetica-Light", size: 11)!)
}
}
}
func addBadge(index: Int, value: Int, color: UIColor, font: UIFont) {
let badgeView = CustomTabBadge()
badgeView.clipsToBounds = true
badgeView.textColor = UIColor.white
badgeView.textAlignment = .center
badgeView.font = font
badgeView.text = String(value)
badgeView.backgroundColor = color
badgeView.tag = index
tabBar.addSubview(badgeView)
self.positionBadges()
}
override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.positionBadges()
}
// Positioning
func positionBadges() {
var tabbarButtons = self.tabBar.subviews.filter { (view: UIView) -> Bool in
return view.isUserInteractionEnabled // only UITabBarButton are userInteractionEnabled
}
tabbarButtons = tabbarButtons.sorted(by: { $0.frame.origin.x < $1.frame.origin.x })
for view in self.tabBar.subviews {
if view is CustomTabBadge {
let badgeView = view as! CustomTabBadge
self.positionBadge(badgeView: badgeView, items:tabbarButtons, index: badgeView.tag)
}
}
}
func positionBadge(badgeView: UIView, items: [UIView], index: Int) {
let itemView = items[index]
let center = itemView.center
let xOffset: CGFloat = 12
let yOffset: CGFloat = -14
badgeView.frame.size = CGSize(width: 17, height: 17)
badgeView.center = CGPoint(x: center.x + xOffset, y: center.y + yOffset)
badgeView.layer.cornerRadius = badgeView.bounds.width/2
tabBar.bringSubview(toFront: badgeView)
}
}
class CustomTabBadge: UILabel {}
It appears that no. You may only set the value.
From Apple's documentation badge is:
Text that is displayed in the upper-right corner of the item with a
surrounding red oval.
You need to specify tab item at index to change badge color, #available in iOS 10 ,
if #available(iOS 10.0, *)
{
self.kAppTabBarController.tabBar.items![1].badgeColor = YOUR_COLOR
}
You can now do it in the storyboard too, by selecting your tab bar item and going to the attributes inspector.
Since iOS 15 has different approach, what worked in my case:
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
let barAppearance = UITabBarItemAppearance()
barAppearance.normal.badgeBackgroundColor = .green
barAppearance.normal.badgeTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.red]
appearance.stackedLayoutAppearance = barAppearance
tabBar.standardAppearance = appearance
YES, But the only possible solution is to create a custom Tabbar and creating your custom tabbar badge icon. You will find many article/code for creating custom tabbar.
// change TabBar BadgeView background Color
-(void)changeTabBarBadgeViewBgColor:(UITabBar*)tabBar {
for (UIView* tabBarButton in tabBar.subviews) {
for (UIView* badgeView in tabBarButton.subviews) {
NSString* className = NSStringFromClass([badgeView class]);
// looking for _UIBadgeView
if ([className rangeOfString:#"BadgeView"].location != NSNotFound) {
for (UIView* badgeSubview in badgeView.subviews) {
NSString* className = NSStringFromClass([badgeSubview class]);
// looking for _UIBadgeBackground
if ([className rangeOfString:#"BadgeBackground"].location != NSNotFound) {
#try {
[badgeSubview setValue:nil forKey:#"image"];
[badgeSubview setBackgroundColor:[UIColor blueColor]];
badgeSubview.clipsToBounds = YES;
badgeSubview.layer.cornerRadius = badgeSubview.frame.size.height/2;
}
#catch (NSException *exception) {}
}
if ([badgeSubview isKindOfClass:[UILabel class]]) {
((UILabel *)badgeSubview).textColor = [UIColor greenColor];
}
}
}
}
}
}
Hm...it's very easy.
[[self tabBarItem] setBadgeColor:[UIColor greenColor]];
Add below lines of code in UITabBarController :
class RootTabBarViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
if #available(iOS 13.0, *) {
let appearance = tabBar.standardAppearance.copy()
setTabBarItemBadgeAppearance(appearance.stackedLayoutAppearance)
setTabBarItemBadgeAppearance(appearance.inlineLayoutAppearance)
setTabBarItemBadgeAppearance(appearance.compactInlineLayoutAppearance)
tabBar.standardAppearance = appearance
if #available(iOS 15.0, *) {
tabBar.scrollEdgeAppearance = appearance
}
}
// Do any additional setup after loading the view.
}
#available(iOS 13.0, *)
private func setTabBarItemBadgeAppearance(_ itemAppearance: UITabBarItemAppearance) {
itemAppearance.normal.badgeBackgroundColor = UIColor.colorBlue207DFF
}
}
Since iOS 15 / Xcode 13, you have to set stackedLayoutAppearance property to change badge color on UITabBarItem. Change just ".blue" with you own color:
if #available(iOS 15.0, *) {
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.stackedLayoutAppearance.normal.badgeBackgroundColor = .blue
UITabBar.appearance().standardAppearance = appearance
UITabBar.appearance().scrollEdgeAppearance = appearance
}
Tested on Xcode 14.1 / iOS 16.
Take a look here # UITabbarItem-CustomBadge.
A complete demonstration is following
it takes only two line of code, if you want to use the default implementation
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//supplying the animation parameter
[UITabBarItem setDefaultAnimationProvider:[[DefaultTabbarBadgeAnimation alloc] init]];
[UITabBarItem setDefaultConfigurationProvider:[[DefaultSystemLikeBadgeConfiguration alloc] init]];
//rest of your code goes following...
return YES;
}

Determine if UIView is visible to the user?

is it possible to determine whether my UIView is visible to the user or not?
My View is added as subview several times into a Tab Bar Controller.
Each instance of this view has a NSTimer that updates the view.
However I don't want to update a view which is not visible to the user.
Is this possible?
Thanks
For anyone else that ends up here:
To determine if a UIView is onscreen somewhere, rather than checking superview != nil, it is better to check if window != nil. In the former case, it is possible that the view has a superview but that the superview is not on screen:
if (view.window != nil) {
// do stuff
}
Of course you should also check if it is hidden or if it has an alpha > 0.
Regarding not wanting your NSTimer running while the view is not visible, you should hide these views manually if possible and have the timer stop when the view is hidden. However, I'm not at all sure of what you're doing.
You can check if:
it is hidden, by checking view.hidden
it is in the view hierarchy, by checking view.superview != nil
you can check the bounds of a view to see if it is on screen
The only other thing I can think of is if your view is buried behind others and can't be seen for that reason. You may have to go through all the views that come after to see if they obscure your view.
This will determine if a view's frame is within the bounds of all of its superviews (up to the root view). One practical use case is determining if a child view is (at least partially) visible within a scrollview.
Swift 5.x:
func isVisible(view: UIView) -> Bool {
func isVisible(view: UIView, inView: UIView?) -> Bool {
guard let inView = inView else { return true }
let viewFrame = inView.convert(view.bounds, from: view)
if viewFrame.intersects(inView.bounds) {
return isVisible(view: view, inView: inView.superview)
}
return false
}
return isVisible(view: view, inView: view.superview)
}
Older swift versions
func isVisible(view: UIView) -> Bool {
func isVisible(view: UIView, inView: UIView?) -> Bool {
guard let inView = inView else { return true }
let viewFrame = inView.convertRect(view.bounds, fromView: view)
if CGRectIntersectsRect(viewFrame, inView.bounds) {
return isVisible(view, inView: inView.superview)
}
return false
}
return isVisible(view, inView: view.superview)
}
Potential improvements:
Respect alpha and hidden.
Respect clipsToBounds, as a view may exceed the bounds of its superview if false.
The solution that worked for me was to first check if the view has a window, then to iterate over superviews and check if:
the view is not hidden.
the view is within its superviews bounds.
Seems to work well so far.
Swift 3.0
public func isVisible(view: UIView) -> Bool {
if view.window == nil {
return false
}
var currentView: UIView = view
while let superview = currentView.superview {
if (superview.bounds).intersects(currentView.frame) == false {
return false;
}
if currentView.isHidden {
return false
}
currentView = superview
}
return true
}
I benchmarked both #Audrey M. and #John Gibb their solutions.
And #Audrey M. his way performed better (times 10).
So I used that one to make it observable.
I made a RxSwift Observable, to get notified when the UIView became visible.
This could be useful if you want to trigger a banner 'view' event
import Foundation
import UIKit
import RxSwift
extension UIView {
var isVisibleToUser: Bool {
if isHidden || alpha == 0 || superview == nil {
return false
}
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
return false
}
let viewFrame = convert(bounds, to: rootViewController.view)
let topSafeArea: CGFloat
let bottomSafeArea: CGFloat
if #available(iOS 11.0, *) {
topSafeArea = rootViewController.view.safeAreaInsets.top
bottomSafeArea = rootViewController.view.safeAreaInsets.bottom
} else {
topSafeArea = rootViewController.topLayoutGuide.length
bottomSafeArea = rootViewController.bottomLayoutGuide.length
}
return viewFrame.minX >= 0 &&
viewFrame.maxX <= rootViewController.view.bounds.width &&
viewFrame.minY >= topSafeArea &&
viewFrame.maxY <= rootViewController.view.bounds.height - bottomSafeArea
}
}
extension Reactive where Base: UIView {
var isVisibleToUser: Observable<Bool> {
// Every second this will check `isVisibleToUser`
return Observable<Int>.interval(.milliseconds(1000),
scheduler: MainScheduler.instance)
.map { [base] _ in
return base.isVisibleToUser
}.distinctUntilChanged()
}
}
Use it as like this:
import RxSwift
import UIKit
import Foundation
private let disposeBag = DisposeBag()
private func _checkBannerVisibility() {
bannerView.rx.isVisibleToUser
.filter { $0 }
.take(1) // Only trigger it once
.subscribe(onNext: { [weak self] _ in
// ... Do something
}).disposed(by: disposeBag)
}
Tested solution.
func isVisible(_ view: UIView) -> Bool {
if view.isHidden || view.superview == nil {
return false
}
if let rootViewController = UIApplication.shared.keyWindow?.rootViewController,
let rootView = rootViewController.view {
let viewFrame = view.convert(view.bounds, to: rootView)
let topSafeArea: CGFloat
let bottomSafeArea: CGFloat
if #available(iOS 11.0, *) {
topSafeArea = rootView.safeAreaInsets.top
bottomSafeArea = rootView.safeAreaInsets.bottom
} else {
topSafeArea = rootViewController.topLayoutGuide.length
bottomSafeArea = rootViewController.bottomLayoutGuide.length
}
return viewFrame.minX >= 0 &&
viewFrame.maxX <= rootView.bounds.width &&
viewFrame.minY >= topSafeArea &&
viewFrame.maxY <= rootView.bounds.height - bottomSafeArea
}
return false
}
I you truly want to know if a view is visible to the user you would have to take into account the following:
Is the view's window not nil and equal to the top most window
Is the view, and all of its superviews alpha >= 0.01 (threshold value also used by UIKit to determine whether it should handle touches) and not hidden
Is the z-index (stacking value) of the view higher than other views in the same hierarchy.
Even if the z-index is lower, it can be visible if other views on top have a transparent background color, alpha 0 or are hidden.
Especially the transparent background color of views in front may pose a problem to check programmatically. The only way to be truly sure is to make a programmatic snapshot of the view to check and diff it within its frame with the snapshot of the entire screen. This won't work however for views that are not distinctive enough (e.g. fully white).
For inspiration see the method isViewVisible in the iOS Calabash-server project
The simplest Swift 5 solution I could come up with that worked in my situation (I was looking for a button embedded in my tableViewFooter).
John Gibbs solution also worked but in my cause I did not need all the recursion.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = scrollView.convert(targetView.bounds, from: targetView)
if viewFrame.intersects(scrollView.bounds) {
// targetView is visible
}
else {
// targetView is not visible
}
}
In viewWillAppear set a value "isVisible" to true, in viewWillDisappear set it to false. Best way to know for a UITabBarController subviews, also works for navigation controllers.
Another useful method is didMoveToWindow()
Example: When you push view controller, views of your previous view controller will call this method. Checking self.window != nil inside of didMoveToWindow() helps to know whether your view is appearing or disappearing from the screen.
This can help you figure out if your UIView is the top-most view. Can be helpful:
let visibleBool = view.superview?.subviews.last?.isEqual(view)
//have to check first whether it's nil (bc it's an optional)
//as well as the true/false
if let visibleBool = visibleBool where visibleBool { value
//can be seen on top
} else {
//maybe can be seen but not the topmost view
}
try this:
func isDisplayedInScreen() -> Bool
{
if (self == nil) {
return false
}
let screenRect = UIScreen.main.bounds
//
let rect = self.convert(self.frame, from: nil)
if (rect.isEmpty || rect.isNull) {
return false
}
// 若view 隐藏
if (self.isHidden) {
return false
}
//
if (self.superview == nil) {
return false
}
//
if (rect.size.equalTo(CGSize.zero)) {
return false
}
//
let intersectionRect = rect.intersection(screenRect)
if (intersectionRect.isEmpty || intersectionRect.isNull) {
return false
}
return true
}
In case you are using hidden property of view then :
view.hidden (objective C) or view.isHidden(swift) is read/write property. So you can easily read or write
For swift 3.0
if(view.isHidden){
print("Hidden")
}else{
print("visible")
}