UIViewController viewDidLayoutSubviews called when transforming a subview - objective-c

I have the following use case:
a view controller (RotationVC) that contains a bit of logic, among others, rotating one of the subviews (by setting its transform to CGAffineTransformMakeRotation)
this RotationVC overrides the viewDidLayoutSubviews method to set its subviews to be as big as the parent view (self.view)
(creating the view controllers and its views is all done programmatically and it has to stay this way; I tried using autoresizingMask with flexible width and flexible height on the subviews but it didn't work)
another view controller (ParentVC) adds the RotationVC as its child, adds its view as its subview and sets its frame at some point to be square - the idea is that when the frame is set, RotationVC viewDidLayoutSubviews will be called and set the subviews to correct sizes (which actually works fine), and then all works fine
(the RotationVC will be used in other view controllers as well and will have different sized, that's the reason it has been extracted to a 'child' view controller and its size should be flexible)
Well, it doesn't work correctly. The problem is that after each rotation, RotationVC viewDidLayoutSubviews method is called, which sets the rotated views frame. This is wrong, as according to Apple's documentation, using the frame property is wrong when the view's transform is anything else than identity (as is in this case). As a result, the rotated view is 'skewed'. Here is the source code down to the delegate, you should be able to paste it to a single file and run. In case you do, what I'm trying to achieve is for the rotated subview to preserve its shape (in this example it's a rectangle):
#import "AppDelegate.h"
#interface RotationVC : UIViewController
#property (nonatomic, weak) UIView *background;
#property (nonatomic, weak) UIView *foreground;
#property (nonatomic) int degrees;
#end
static const double DURATION = 0.5;
#implementation RotationVC
- (void)viewDidLoad {
[super viewDidLoad];
UIView *tmp = [UIView new];
self.background = tmp;
self.background.layer.borderWidth = 2;
self.background.layer.borderColor = [UIColor redColor].CGColor;
[self.view addSubview:self.background];
tmp = [UIView new];
self.foreground = tmp;
self.foreground.layer.borderWidth = 2;
self.foreground.layer.borderColor = [UIColor blueColor].CGColor;
[self.view addSubview:self.foreground];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.background.frame = self.view.bounds;
CGAffineTransform tmp = self.foreground.transform;
self.foreground.transform = CGAffineTransformIdentity;
self.foreground.frame = self.view.bounds;
self.foreground.transform = tmp;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self performSelector:#selector(rotate) withObject:self];
}
- (void)rotate {
self.degrees = (self.degrees + 15) % 360;
[UIView animateWithDuration:DURATION
delay:0
options:UIViewAnimationOptionCurveLinear|UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.foreground.transform = CGAffineTransformMakeRotation(self.degrees * M_PI/180);
}
completion:^(BOOL finished) {
[self performSelector:_cmd withObject:self afterDelay:0];
}];
}
#end
#interface ParentVC : UIViewController
#property (nonatomic, weak) RotationVC *rotationVC;
#end
#implementation ParentVC
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
RotationVC *tmp = [RotationVC new];
self.rotationVC = tmp;
[self addChildViewController:self.rotationVC];
[self.rotationVC didMoveToParentViewController:self];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.rotationVC.view];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.rotationVC.view.bounds = CGRectMake(0, 0, 100, 100);
self.rotationVC.view.center = self.view.center;
}
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UINavigationController *navi = [[UINavigationController alloc] initWithRootViewController:[ParentVC new]];
navi.navigationBarHidden = YES;
self.window.rootViewController = navi;
[self.window makeKeyAndVisible];
return YES;
}
#end
There is a hack that works - in the viewDidLayoutSubviews method, store the rotated view's transform to a temp variable, set the view's transform to identity, set the frame, and set it to rotation again - this is ugly as hell and I bet there has to be a better way. I was also able to devise a few other workarounds, but each one was uglier than the previous one...
Would anybody have a hint as to what to do in this case?

I was able to solve the issue. Just use the following code:
self.foreground.bounds = self.view.bounds;
self.foreground.center = self.background.center;
instead of setting the frame.
This is explained in https://developer.apple.com/library/ios/documentation/windowsviews/conceptual/viewpg_iphoneos/WindowsandViews/WindowsandViews.html#//apple_ref/doc/uid/TP40009503-CH2-SW7:
Important: If a view’s transform property is not the identity transform, the value of that view’s frame property is undefined and must be ignored. When applying transforms to a view, you must use the view’s bounds and center properties to get the size and position of the view. The frame rectangles of any subviews are still valid because they are relative to the view’s bounds.
I always thought that setting the frame is setting the bounds and the center at the same time, but apparently this is very untrue.

Related

How to set fill color property of custom UIView (a circle) from View Controller

I'm guessing I don't know enough about Quartz or CAShapeLayer manipulations, but I wanted to know how to change the fill color to custom UIView that I have.
Here's the implementation for DotView:
#define kCustomBlue [UIColor colorWithRed:181.0/255 green:228.0/255 blue:226.0/255 alpha:1]
#implementation DotView
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.backgroundColor = [UIColor clearColor];
self.opaque = YES;
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder
{
if ((self = [super initWithCoder:coder])) {
self.backgroundColor = [UIColor clearColor];
self.opaque = YES;
}
return self;
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CAShapeLayer *circleLayer = [CAShapeLayer layer];
[circleLayer setPath:[[UIBezierPath bezierPathWithOvalInRect:rect] CGPath]];
circleLayer.fillColor = kCustomBlue.CGColor;
[self.layer addSublayer:circleLayer];
}
#end
After placing a DotView object ((nonatomic, weak) IBOutlet in my ViewController interface) in my Storyboard, I run the code and everything is fine and dandy. That object in my ViewController interface is called dot1
I want to have a property #property (nonatomic, strong) UIColor* fillingColor; which sets the fill color for the view. How do I implement that correctly?
The idea is this: there is a tap gesture recognizer object attached to the dot1 view, and everytime I tap the dot, the color changes from blue to black (then black to blue).
I'm using XCode 9.3 and have an iPhone 7 running iOS 11.2
Thanks, Anthony
Add the property to the .h:
#interface DotView: UIView
// Add this to everything else you have
#property (nonatomic, strong) UIColor* fillingColor;
#end
Then in the .m, override the setter:
- (void)setFillingColor:(UIColor *)color {
_fillingColor = color;
[self setNeedsDisplay];
}
Then in drawRect:, use your fillingColor property for the fill color:
circleLayer.fillColor = self.fillingColor.CGColor;
Please note that you do not want to be adding layers over and over every time drawRect: is called. You don't even need to use layers for this. Just fill the UIBezierPath.

How can I redraw an image in an custom NSView for an NSStatusItem?

I am having some troubles understanding how to wire a custom NSView for an NSMenuItem to support both animation and dragging and dropping. I have the following subclass of NSView handling the bulk of the job. It draws my icon when the application launches correctly, but I have been unable to correctly setup the subview to change when I invoke the setIcon function from another caller. Is there some element of the design that I am missing?
TrayIconView.m
#import "TrayIconView.h"
#implementation TrayIconView
#synthesize statusItem;
static NSImageView *_imageView;
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
statusItem = nil;
isMenuVisible = NO;
_imageView = [[NSImageView alloc] initWithFrame:[self bounds]];
[self addSubview:_imageView];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Draw status bar background, highlighted if menu is showing
[statusItem drawStatusBarBackgroundInRect:[self bounds]
withHighlight:isMenuVisible];
}
- (void)mouseDown:(NSEvent *)event {
[[self menu] setDelegate:self];
[statusItem popUpStatusItemMenu:[self menu]];
[self setNeedsDisplay:YES];
}
- (void)rightMouseDown:(NSEvent *)event {
// Treat right-click just like left-click
[self mouseDown:event];
}
- (void)menuWillOpen:(NSMenu *)menu {
isMenuVisible = YES;
[self setNeedsDisplay:YES];
}
- (void)menuDidClose:(NSMenu *)menu {
isMenuVisible = NO;
[menu setDelegate:nil];
[self setNeedsDisplay:YES];
}
- (void)setIcon:(NSImage *)icon {
[_imageView setImage:icon];
}
TrayIconView.h
#import <Cocoa/Cocoa.h>
#interface TrayIconView : NSView
{
BOOL isMenuVisible;
}
#property (retain, nonatomic) NSStatusItem *statusItem;
- (void)setIcon:(NSImage *)icon;
#end
The solution to this problem was actually outside of the view detailed here. The caller of the interface was being double instantiated on accident, thus nulling out the reference to the previously created NSView. After correcting that concern the app draws and works just fine.
With regard to dragging, I just implemented a subclass of NSView that implemented the Cocoa draggable protocol and added it as a subview to this parent class. That allows dragging onto the currently established NSRect that contains the menubar icon.

setting self of UIView background color

i'm trying to do this from inside the .m of a custom view class that is not being loaded from the XIB, but rather programmatically:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor=[UIColor redcolor];
}
return self;
}
i have the same result whether i put the background color in the initWithFrame or other methods. the background color property doesn't take. from the controller, which owns this custom view, i can set the background color fine, with:
self.mycustomview.backgroundColor=[UIColor redcolor];
But I'd like to do this from within the custom view itself, keep stuff like this independent. both the controller and the custom view import UIKit.
I also tried this, which was available from Code Sense:
self.View.backgroundColor=[UIColor redcolor];
but that doesn't work either. i tried both view and View here. I'm sure I'm overlooking something very obvious.
in the view controller i have this, and it works fine. the custom view is called "mapButtons.h":
- (void)viewDidLoad
{
CGRect frame=CGRectMake(0, 0, 320, 460);
self.mapButtons=[[mapButtons alloc] initWithFrame:frame];
self.mapButtons.backgroundColor=[UIColor redColor];
[self.view addSubview:self.mapButtons];
the .h of the custom view is this:
#import <UIKit/UIKit.h>
#interface mapButtons : UIView
If your view is getting created from a XIB (i.e. you added it to some other view using Interface Builder), -initWithFrame: is not going to get called. An object being loaded from a XIB receives -initWithCoder: instead. Try this:
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if(self)
{
self.backgroundColor = [UIColor redColor];
}
return self;
}
I have tested again and this is the full source for what I am doing that works
// MapButtons.h
#import <UIKit/UIKit.h>
// As a note you normally define class names starting with a capital letter
// but I did test this with mapButtons as you had it
#interface MapButtons : UIView
#end
// MapButtons.m
#import "mapButtons.h"
#implementation mapButtons
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor redColor];
}
return self;
}
#end
// TestAppDelegate.m
#implementation TestAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
MapButtons *view = [[MapButtons alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[self.window addSubview:view];
[self.window makeKeyAndVisible];
return YES;
}
The fact that xcode is not auto completing is odd but I seem to have this issue intermittently so I have no real solution. People sometimes suggest deleting the projects derived data and restarting xcode.

How to Implement ScrollView In TableViewCell

Hey guys I'm trying to implement a scroll with page control, like the iPhone home screen, but in a uitableview cell.
What I tried to do to attempt this is create a custom table view cell with a xib file, and placed the uipagecontrol and uiscrollview on it, and connected it with iboutlets to the uitableviewcell.
This is the code in the .h file for the custom cell.
#interface ScrollableCell : UITableViewCell <UIScrollViewDelegate>
#property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
#property (nonatomic, strong) IBOutlet UIPageControl *pageControl;
#property (nonatomic, strong) NSMutableArray *viewControllers;
This is the code from the .m file for the custom cell after synthesizing the properties.
- (CGSize)contentSizeForPagingScrollView {
CGRect bounds = self.scrollView.bounds;
return CGSizeMake(bounds.size.width * [self.viewControllers count] , bounds.size.height );
}
#- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
UIView *blueView = [[UIView alloc] initWithFrame:CGRectZero];
[blueView setBackgroundColor:[UIColor blueColor]];
UIView *redView = [[UIView alloc] initWithFrame:CGRectZero];
[redView setBackgroundColor:[UIColor redColor]];
UIView *yellowView = [[UIView alloc] initWithFrame:CGRectZero];
[yellowView setBackgroundColor:[UIColor yellowColor]];
self.viewControllers = [NSArray arrayWithObjects:blueView, redView, yellowView,nil];
for (UIView *view in self.viewControllers) {
[view setFrame:CGRectMake([self.viewControllers indexOfObject:view]*self.scrollView.frame.size.width+5,
5,
self.scrollView.frame.size.width-10,
self.scrollView.frame.size.height-10.0)];
[self.scrollView addSubview:view];
}
self.pageControl.numberOfPages = [self.viewControllers count];
self.pageControl.currentPage = 0;
self.scrollView.pagingEnabled = YES;
self.scrollView.backgroundColor = [UIColor blackColor];
self.scrollView.showsVerticalScrollIndicator = NO;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.contentSize = [self contentSizeForPagingScrollView];
self.scrollView.delegate = self;
[self.contentView addSubview:self.scrollView];
[self.contentView addSubview:self.pageControl];
}
return self;
}
-(void) scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
self.pageControl.currentPage = page;
}
Hope you guys can help me get this working. If there is a different approach/ better approach that what I'm attempting, let me know.
For visual reference of what I'm trying to achieve, i think the pulse news app fits the bill
http://itunes.apple.com/us/app/pulse-news-for-iphone/id377594176?mt=8
its a table view, with horizontal scrolling on each cell.
Thanks.
If there is a different approach/ better approach that what I'm attempting, let me know.
For visual reference of what I'm trying to achieve, i think the pulse news app fits the bill.
If your main reference for designing this is the Pulse app, you should know that the developers actually did NOT resort to scrollviews. The Pulse app is all tableviews. One main vertical tableview and several horizontal "hacked" tableviews inside each cell that composes the vertical one.
Its quite a smart implementation for a tableview. But who better to explain this to you than the developers of Pulse themselves; they were invited to a lecture at Stanford for their iOS programming course.
Its on iTunes U and it's at 6 min in the lecture called 10 Hacks to Go From Idea To #1 App. You should watch it and maybe rethink your app design if it suits you better.

UINavigationController: Apply Common Padding/Margin to all Popped View Controller's Views

I have a UINavigationController and I would like the view of every view controller that is popped onto the stack to have a common padding/margin (e.g. 25 pixels on all sides). What is the best way to accomplish this?
I originally thought that I could implement UINavigationControllerDelegate and inside the navigationController:didShowViewController:animated or navigationController:willShowViewController:animated methods, simply change the frame of the view controller that was about to be displayed. This does not seem to have an effect though.
I tried to do the same thing inside the view controller's viewDidAppear and viewWillAppear methods, but this also did not work. Ideally, I don't want to put any logic in the controllers anyway, as they may not always be used inside a navigation controller.
One last idea that I haven't tried yet is to create a "wrapper" UIViewController that would actually get pushed onto this stack. This wrapper would add the real view controller's view as a subview with a frame that would provide the desired margin. The downside here is that I would need to subclass UINavigationController and override pushViewController:animated, where the wrapper would be initialized and pushed. Apple's documentation indicates that UINavigationController is not meant to be subclassed.
Thanks in advance.
I solved this by putting a "wrapper" UIView around the UIViewController's view instead of the UIViewController itself. The wrapper view then pads the subview by setting the subview's frame in the layoutSubviews method.
I've attached the code I used for convenience. To use, replace your UINavigationController with the PaddedNavigationController, and set the PaddedNavigationController's insets property.
PaddedNavigationController.h:
#import <Foundation/Foundation.h>
#interface PaddedNavigationController : UINavigationController
{
UIEdgeInsets _insets;
}
#property (nonatomic, assign) UIEdgeInsets insets;
#end
PaddedNavigationController.m:
#import "PaddedNavigationController.h"
#interface PaddedView : UIView
{
UIView *_view;
UIEdgeInsets _insets;
}
#property (nonatomic, assign) UIEdgeInsets insets;
+ (PaddedView *) wrapView:(UIView *)view withInsets:(UIEdgeInsets)insets;
- (id) initWithView:(UIView *)view insets:(UIEdgeInsets)insets;
#end
#implementation PaddedNavigationController
#synthesize insets = _insets;
- (void) pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
//check if the UIViewController's view has already been wrapped by the PaddedView; don't want to wrap it twice
if(![viewController.view isKindOfClass:[PaddedView class]])
{
viewController.view = [PaddedView wrapView:viewController.view withInsets:self.insets];
}
[super pushViewController:viewController animated:animated];
}
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets;
//loop through this navigation controller's view controllers and set the new insets on any PaddedViews
for(UIViewController *viewController in self.viewControllers)
{
if([viewController.view isKindOfClass:[PaddedView class]])
{
PaddedView *padded = (PaddedView *)viewController.view;
padded.insets = insets;
}
}
}
#end
#implementation PaddedView
#synthesize insets = _insets;
+ (PaddedView *) wrapView:(UIView *)view withInsets:(UIEdgeInsets)insets
{
return [[[PaddedView alloc] initWithView:view insets:insets] autorelease];
}
- (id) initWithView:(UIView *)view insets:(UIEdgeInsets)insets
{
if(self = [super initWithFrame:view.frame])
{
_insets = insets;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_view = [view retain];
[self addSubview:view];
}
return self;
}
- (void) dealloc
{
[_view release];
[super dealloc];
}
- (void) layoutSubviews
{
//apply the insets to the subview
_view.frame = CGRectMake(self.insets.left, self.insets.top, self.frame.size.width - self.insets.left - self.insets.right, self.frame.size.height - self.insets.top - self.insets.bottom);
}
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets;
//we need to re-layout the subviews as the insets have changed
[self layoutSubviews];
}
#end