UIPopoverController or pop-up window (hereinafter simply “popover”) element is not new. On Habré there is one introductory article on this topic and several mentions in other topics. Most often, poppers are used “as is” and do not require any modifications, but in some projects there is a need to change the appearance of this element. Just about how to do it and this article will be.appearance method of the UINavigationBar class was used [[UINavigationBar appearance] setTintColor: [UIColor colorWithRed:0.481 green:0.065 blue:0.081 alpha:1.000]]; [[UINavigationBar appearance] setBackgroundImage:[UIImage imageNamed:@"navbar"] forBarMetrics:UIBarMetricsDefault]; 
UINavigationController inside the popover.
UINavigationBar class, if it is displayed inside the popover [[UINavigationBar appearanceWhenContainedIn:[UIPopoverController class], nil] setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; [[UINavigationBar appearanceWhenContainedIn:[UIPopoverController class], nil] setTintColor:[UIColor clearColor]]; 
popoverBackgroundViewClass property of the popoverBackgroundViewClass class UIPopoverController . Our task is to inherit the UIPopoverBackgroundView class, strictly following the documentation.UIPopoverBackgroundViewUIImageView class for drawing the background and arrows. All this is “in words,” I personally find it easier to read the text if it includes illustrations, so I will try to fill this “gap”. In parallel, we will start writing the implementation of our specific subclass UIPopoverBackgroundView . The first thing we will do is simply inherit it and leave it so far without implementation. #import <UIKit/UIPopoverBackgroundView.h> @interface MBPopoverBackgroundView : UIPopoverBackgroundView @end UIPopoverController
UIPopoverController consists of an arrow (Arrow), a background (Background), content or content (Content View), and a UIView in which all this stuff is contained and drawn.
In essence, the “arrow” in this context is a purely figurative term. We are limited only by our own imagination and common sense choosing the look of the arrow. This can be a dotted line, a curve, an arbitrary picture. We can just use a UIView with an overridden draw method and draw with gl*** functions, we can use an animated UIImageView , etc. The only thing to remember is the width of the base of the arrow ( arrowBase ) and its height ( arrowHeight ) remain unchanged for all instances of our class. Although this limitation can be bypassed to some extent, this will be discussed later.UIImageView to represent the arrow, following Apple's advice. Also pay attention to the class +(CGFloat)arrowBase and +(CGFloat)arrowHeight . By default, they both throw an exception, so we have to override them in our subclass. @interface MBPopoverBackgroundView () // image view @property (nonatomic, strong) UIImageView *arrowImageView; @end @implementation MBPopoverBackgroundView @synthesize arrowImageView = _arrowImageView; // (arrow base) + (CGFloat)arrowBase { // return [UIImage imageNamed:@"popover-arrow.png"].size.width; } // (arrow height) + (CGFloat)arrowHeight { // return [UIImage imageNamed:@"popover-arrow.png"].size.height; } // - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (!self) return nil; // image view self.arrowImageView = [[UIImageView alloc] initWithImage:@"popover-arrow.png"]; [self addSubview:_arrowImageView]; return self; } @end @property (nonatomic, readwrite) UIPopoverArrowDirection arrowDirection; @property (nonatomic, readwrite) CGFloat arrowOffset; arrowDirection ) tells us where the arrow points (up, down, left, right) and where it is actually located. The offset of the arrow ( arrowOffset ) is the distance from the center of our view to the line passing through the center of the arrow, in general, look at the illustration, everything is clearly shown there, the displacements are marked in blue. The up and left offsets are negative.
@interface MBPopoverBackgroundView () // @property (nonatomic, readwrite) UIPopoverArrowDirection arrowDirection; @property (nonatomic, readwrite) CGFloat arrowOffset; @end @implementation MBPopoverBackgroundView @synthesize arrowDirection = _arrowDirection; @synthesize arrowOffset = _arrowOffset; @end MBPopoverBackgroundView let our MBPopoverBackgroundView that it’s time to put things in order and place the children (subviews) in places, i.e. call setNeedsLayout . This, in turn, will lead to a call to layoutSubviews at the next right moment (when the operating system solves). About the implementation of layoutSubviews will be discussed in detail a little later. - (id)initWithFrame:(CGRect)frame { // *** *** [self addObserver:self forKeyPath:@"arrowDirection" options:0 context:nil]; [self addObserver:self forKeyPath:@"arrowOffset" options:0 context:nil]; return self; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // // setNeedsLayout [self setNeedsLayout]; } - (void)dealloc { [self removeObserver:self forKeyPath:@"arrowDirection"]; [self removeObserver:self forKeyPath:@"arrowOffset"]; // *** "" *** [super dealloc]; } UIImageView for a specific implementation. But, while the arrow does not change its size, the background behaves completely differently. In your application, you will use popovers for a variety of purposes and stuff contents of various sizes inside. The background should look equally good both for a small tooltip and an unimaginable popover on the floor of the screen. Apple recommends using stretchable images; the UIImageView class provides the resizableImageWithCapInsets:(UIEdgeInsets)capInsets method for these purposes resizableImageWithCapInsets:(UIEdgeInsets)capInsets . For example, I created a simple background, a rectangle of size 128x128 with rounded corners and filled with one color without gradients, shadows and other effects. Name the file "popover-background.png". @property (nonatomic, strong) UIImageView *backgroundImageView; // *** @synthesize backgroundImageView = _backgroundImageView; - (id)initWithFrame:(CGRect)frame { // *** UIEdgeInsets bgCapInsets = UIEdgeInsetsMake(12, 12, 12, 12); UIImage *bgImage = [[UIImage imageNamed:@"popover-backgroung.png"] resizableImageWithCapInsets:bgCapInsets]; self.backgroundImageView = [[UIImageView alloc] initWithImage:bgImage]; [self addSubview:_backgroundImageView]; // *** } UIEdgeInsets options are specified using indents ( UIEdgeInsets ). Specific values depend on the selected image. In my case, for example, the radius of the rounding of corners is 10, so, in theory, the indent could be taken equal to 10 from all borders, but this is not significant.
UIPopoverBackgroundView we have no influence on the content and its size, on the contrary, it is the size of the content that determines the size of the popover, and hence the size of the UIPopoverBackgroundView .UIPopoverController ready to render the popover, it knows exactly the content size and position from which to draw the popover, it only remains to find out how much more to add along the edges to fit the arrow and the background, in other words, to calculate the frame property for our MBPopoverBackgroundView .+(CGFloat)arrowHeight and +(UIEdgeInsets)contentViewInsets . The first tells the height of the arrow, the second tells how much background is more content, returning indents from the edges of the content to the edges of the background. Using all this information, UIPopoverController chooses the direction for the arrow and initializes an object of the UIPopoverBackgroundView class (or rather our particular subclass), setting it with specific dimensions, after which we should place our arrow and background as it should.contentViewInsets . For example, we indent 10 at all edges. You can also set negative indents, I don’t think that something good will turn out, but you can ... + (UIEdgeInsets)contentViewInsets { // return UIEdgeInsetsMake(10, 10, 10, 10); } 
UIPopoverBackgroundView .layoutSubviews method. #pragma mark - Subviews Layout // , setNeedsLayout - (void)layoutSubviews { // // CGRect bgRect = self.bounds; // , "" // , / , BOOL cutWidth = (_arrowDirection == UIPopoverArrowDirectionLeft || _arrowDirection == UIPopoverArrowDirectionRight); // , bgRect.size.width -= cutWidth * [self.class arrowHeight]; BOOL cutHeight = (_arrowDirection == UIPopoverArrowDirectionUp || _arrowDirection == UIPopoverArrowDirectionDown); // , bgRect.size.height -= cutHeight * [self.class arrowHeight]; // , origin point ( ) // ( ) ( ) if (_arrowDirection == UIPopoverArrowDirectionUp) { bgRect.origin.y += [self.class arrowHeight]; } else if (_arrowDirection == UIPopoverArrowDirectionLeft) { bgRect.origin.x += [self.class arrowHeight]; } // _backgroundImageView.frame = bgRect; // - (arrowDirection) (arrowOffset) // , image view // ( transformations), // : CGRect arrowRect = CGRectZero; UIEdgeInsets bgCapInsets = UIEdgeInsetsMake(12, 12, 12, 12); // switch (_arrowDirection) { case UIPopoverArrowDirectionUp: _arrowImageView.transform = CGAffineTransformMakeScale(1, 1); // - // : frame, bounds, bounds arrowRect = _arrowImageView.frame; // origin arrowRect.origin.x = self.bounds.size.width / 2 + _arrowOffset - arrowRect.size.width / 2; arrowRect.origin.y = 0; break; case UIPopoverArrowDirectionDown: _arrowImageView.transform = CGAffineTransformMakeScale(1, -1); // () arrowRect = _arrowImageView.frame; // origin arrowRect.origin.x = self.bounds.size.width / 2 + _arrowOffset - arrowRect.size.width / 2; arrowRect.origin.y = self.bounds.size.height - arrowRect.size.height; break; case UIPopoverArrowDirectionLeft: _arrowImageView.transform = CGAffineTransformMakeRotation(-M_PI_2); // 90 arrowRect = _arrowImageView.frame; // origin arrowRect.origin.x = 0; arrowRect.origin.y = self.bounds.size.height / 2 + _arrowOffset - arrowRect.size.height / 2; // - // , // , bgCapInsets.bottom, // arrowRect.origin.y = fminf(self.bounds.size.height - arrowRect.size.height - bgCapInsets.bottom, arrowRect.origin.y); // arrowRect.origin.y = fmaxf(bgCapInsets.top, arrowRect.origin.y); break; case UIPopoverArrowDirectionRight: _arrowImageView.transform = CGAffineTransformMakeRotation(M_PI_2); // 90 arrowRect = _arrowImageView.frame; arrowRect.origin.x = self.bounds.size.width - arrowRect.size.width; arrowRect.origin.y = self.bounds.size.height / 2 + _arrowOffset - arrowRect.size.height / 2; // UIPopoverArrowDirectionLeft arrowRect.origin.y = fminf(self.bounds.size.height - arrowRect.size.height - bgCapInsets.bottom, arrowRect.origin.y); arrowRect.origin.y = fmaxf(bgCapInsets.top, arrowRect.origin.y); break; default: break; } // _arrowImageView.frame = arrowRect; } @interface MBPopoverBackgroundView : UIPopoverBackgroundView // + (void)initialize; // ( ) + (void)cleanup; // ( ) + (void)setArrowImageName:(NSString *)imageName; // + (void)setBackgroundImageName:(NSString *)imageName; // + (void)setBackgroundImageCapInsets:(UIEdgeInsets)capInsets; // + (void)setContentViewInsets:(UIEdgeInsets)insets; // @end MBPopoverBackgroundView , one heir for each appearance, or call set*** for MBPopoverBackgroundView each time before creating a popover different from the previous one. In short, flexibility ... // @interface MBPopoverBackgroundViewBlue : MBPopoverBackgroundView @end // - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // [MBPopoverBackgroundView initialize]; // [MBPopoverBackgroundView setArrowImageName:@"popover-arrow-red.png"]; [MBPopoverBackgroundView setBackgroundImageName:@"popover-background-red.png"]; [MBPopoverBackgroundView setBackgroundImageCapInsets:UIEdgeInsetsMake(12, 12, 12, 12)]; [MBPopoverBackgroundView setContentViewInsets:UIEdgeInsetsMake(10, 10, 10, 10)]; // "" [MBPopoverBackgroundViewBlue setArrowImageName:@"popover-callout-dotted-blue.png"]; [MBPopoverBackgroundViewBlue setBackgroundImageName:@"popover-background-blue.png"]; [MBPopoverBackgroundViewBlue setBackgroundImageCapInsets:UIEdgeInsetsMake(15, 15, 15, 15)]; [MBPopoverBackgroundViewBlue setContentViewInsets:UIEdgeInsetsMake(20, 20, 20, 20)]; // *** } // { UIPopoverController *popoverCtl = ...; popoverCtl.popoverBackgroundViewClass = [MBPopoverBackgroundView class]; // popoverCtl.popoverBackgroundViewClass = [MBPopoverBackgroundViewBlue class]; // // *** } MBPopoverBackgroundView sources and usage examples are on github .-fno-objc-arc flag if you use it in a project with ARC enabled, or remove those multiple autorelease , retain , release and dealloc calls that are in the code. In the latter case, I have no idea how long the s_customValuesDic static dictionary will live because the retain is not explicitly sent to it, although according to the ARC logic it will not touch the static object until the application is completed. And I don’t even think that storing values in this way is the best solution, even though it works stably and reliably.Source: https://habr.com/ru/post/137851/
All Articles