I'm creating a custom animatable property on a CAGradientLayer derived class that has to change other built in animatable properties on the CAGradientLayer base class and was wondering what the best approach is. Currently I'm updating the dependant properties in the display method:
#implementation CustomGradientLayer
#dynamic myCustomProperty;
+ (BOOL) needsDisplayForKey: (NSString*)aKey
{
BOOL needsDisplay = [aKey isEqualToString: #"myCustomProperty"];
if (!needsDisplay)
{
needsDisplay = [super needsDisplayForKey: aKey];
}
return needsDisplay;
}
- (void) display
{
CGFloat myCustomProperty = [self.presentationLayer myCustomProperty];
[CATransaction begin];
[CATransaction setDisableActions: YES];
// Update dependant properties on self
[CATransaction commit];
[super display];
}
Is it possible to safely update the dependant properties in a custom property setter instead without affecting the underlying CALayer magic?
I found the solution but for anyone interested you can use the following method without interfering with the CALayar stuff:
- (void) didChangeValueForKey: (NSString*)aKey (void) didChangeValueForKey: (NSString*)aKey
{
// Update dependent properties
}
I have a UIView
#property (nonatomic, strong) IBOutlet UIView *previewView;
previewView will ultimately display the preview of what is going on in my camera on my iPhone using the following preview layer.
#property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
I have constraints set up on my previewView in storyboards and therefore am running into the following issue.
self.previewLayer.frame = self.previewView.bounds; // This doesn't work
self.previewLayer.videoGravity = AVLayerVideoGravityResize;
[self.previewView.layer addSublayer:self.previewLayer];
previewLayer gets added to the previewView subLayer, but it does not show in the right parameters (width and height). I think this is due to using constraints on autolayout.
How would I fix this?
Update: I also tried the following:
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspect;
CALayer *rootLayer = [previewView layer];
rootLayer.frame = self.previewView.bounds;
[rootLayer setMasksToBounds:YES];
[self.previewLayer setFrame:[rootLayer bounds]];
[rootLayer addSublayer:self.previewLayer];
And it appears to show like this: http://cl.ly/image/3T3o0O1E3U17
Getting close, but still very much off.
Okay, I solved it.
The problem is that when you use autolayout, the frames/bounds of your UIViewController's subviews can change after you have set up your previewLayer, without the previewLayer itself being updated accordingly! This is because the layout of CALayers is solely controlled by one's own code (see also this related answer: https://stackoverflow.com/a/27735617/623685).
To solve this, you have to override the viewDidLayoutSubviews method of your UIViewController to update the previewLayer's position and bounds (and possibly others) every time the subviews' layout changes.
In the following, I will describe how I solved it in detail:
My case was actually a bit more complicated, because I am using the OpenCV iOS framework 2.4.9. Here, the preview layer is created and managed by an OpenCV class called CvVideoCamera. Therefore I had to create a subclass of CvVideoCamera, to which I added the following method:
- (void)updatePreviewLayer
{
self.customPreviewLayer.bounds = CGRectMake(0, 0, self.parentView.frame.size.width, self.parentView.frame.size.height);
[self layoutPreviewLayer];
}
Explanation: parentView is a UIView you pass to CvVideoCamera in order to specify where the preview layer should be positioned and how big it should be. customPreviewLayer is the preview layer managed by CvVideoCamera. And layoutPreviewLayer is a method that basically updates the layer's position: https://github.com/Itseez/opencv/blob/2.4/modules/highgui/src/cap_ios_video_camera.mm#L211
Then I simply called this method from my UIViewController like so:
- (void)viewDidLayoutSubviews
{
NSLog(#"viewDidLayoutSubviews");
[super viewDidLayoutSubviews];
[self.myCameraView updatePreviewLayer];
}
The main idea is the same - when using Autolayout, the frame of the preview layer needs to be set after all view constraints have been applied. The viewDidLayoutSubviews method is used for this - same as in the original answer but without complications of using any frameworks other than the Apple's AVFoundation.
The following code is based on an Apple's sample (AVCam-iOS: Using AVFoundation to Capture Images and Movies). But the code was modified to use updateViewConstraints method to programmatically set all constraints instead of the Storyboard approach used in the sample. Relevant fragments are shown below:
#interface QRScannerViewController ()
#property (nonatomic) UIView *previewView;
#end
#implementation QRScannerViewController
{
AVCaptureSession *_captureSession;
AVCaptureVideoPreviewLayer *_captureVideoPreviewLayer;
dispatch_queue_t _sessionQueue;
}
#pragma mark - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.previewView];
_captureSession = [AVCaptureSession new];
_captureVideoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_captureSession];
_captureVideoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
_sessionQueue = dispatch_queue_create("av_session_queue", DISPATCH_QUEUE_SERIAL);
[self.view setNeedsUpdateConstraints];
dispatch_async(_sessionQueue, ^{
[self configureCaptureSession];
});
[self.previewView.layer addSublayer:_captureVideoPreviewLayer];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (_isCaptureDeviceConfigured) {
[_captureSession startRunning];
}
}
- (void)viewDidLayoutSubviews
{
_captureVideoPreviewLayer.frame = self.previewView.bounds;
}
#pragma mark - getters / initializers
- (UIView *)previewView
{
if (_previewView) {
return _previewView;
}
_previewView = [UIView new];
_previewView.backgroundColor = [UIColor blackColor];
_previewView.translatesAutoresizingMaskIntoConstraints = NO;
return _previewView;
}
- (void)configureCaptureSession
{
[_captureSession beginConfiguration];
// Code omitted, only relevant to the preview layer is included. See the Apple's sample for full code.
...
_captureVideoPreviewLayer.connection.videoOrientation = initialVideoOrientation;
...
}
#pragma mark - Autolayout
- (void)updateViewConstraints
{
[self.previewView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor constant:0.0].active = YES;
[self.previewView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor constant:-40.0].active = YES;
[self.previewView.widthAnchor constraintEqualToConstant:300].active = YES;
[self.previewView.heightAnchor constraintEqualToAnchor:self.previewView.widthAnchor constant:0.0].active = YES;
[super updateViewConstraints];
}
How would I create an overlay that colors the entire map a certain color? Then I need to be able to place annotations on top of it. Any ideas? Thanks.
What you want is MKOverlay and MKOverlayView.
You can find apple's code in one of the apps mentioned in 'Related sample code'. in above protocol and class reference page.
EDIT : As Per the comments previous code was not working as-is. Heres a MKMapDimOverlay GitHub project which you can simply integrate using CocoaPods. I have also made the relevant changes in following code in the answer.
To explain briefly, following is code for adding a dark overlay on entire map.
You need to create an overlay and add it to the map view.
MKMapDimOverlay *dimOverlay = [[MKMapDimOverlay alloc] initWithMapView:MapView];
[mapView addOverlay: dimOverlay];
Create and return MKOverlayView for the specific MKOverlay in 'viewForOverlay' delegate method
-(MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay {
if([overlay isMemberOfClass:[MKMapDimOverlay class]]) {
MKMapDimOverlayView *dimOverlayView = [[MKMapDimOverlayView alloc] initWithOverlay:overlay];
return dimOverlayView;
}
}
Since all you want is a colored overlay covering the map, your overlay and overlay view implementation will be very simple.
DimOverlay.m
#interface DimOverlay ()
#property (nonatomic) CLLocationCoordinate2D dimOverlayCoordinates;
#end
#implementation DimOverlay
-(id)initWithMapView:(MKMapView *)mapView {
self = [super init];
if(self)
{
self.dimOverlayCoordinates = mapView.centerCoordinate;
}
return self;
}
-(CLLocationCoordinate2D)coordinate {
return self.dimOverlayCoordinates;
}
-(MKMapRect)boundingMapRect {
return MKMapRectWorld;
}
#end
DimOverlayView.m
#implementation DimOverlayView
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)ctx {
/*
You can allow custom colors and opacity values.
Simply add UIColor and CGFloat properties in the overlay view class
and use those properties instead of the default hardcodes values below.
*/
CGContextSetAlpha(ctx, 0.85);
CGContextSetFillColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextFillRect(ctx, [self rectForMapRect:mapRect]);
}
#end
All I want to do is change the font color of the UIDatePicker. I've researched other questions but they're all involving changing other properties and customizing the entire look. All I want to do is just change the font color from black to white. I find it hard to believe that I can't do such a seemingly simple task. And why wouldn't the tint color affect it? Does it even do anything?
All I need (on iOS 8.x and 9.0) is this one line to change the font color:
[my_date_picker setValue:[UIColor whiteColor] forKey:#"textColor"];
No subclassing or invoking of private APIs...
Note: today's current date will still be highlighted (in newer iOS versions). You can get around this by using the following code:
if ([my_date_picker respondsToSelector:sel_registerName("setHighlightsToday:")]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
[my_date_picker performSelector:#selector(setHighlightsToday:) withObject:[NSNumber numberWithBool:NO]];
#pragma clang diagnostic pop
}
As of Swift 2.1:
picker.setValue(UIColor.whiteColor(), forKey: "textColor")
picker.sendAction("setHighlightsToday:", to: nil, forEvent: nil)
let date = NSDate()
picker.setDate(date, animated: false)
Instead of "today" you will see the current day,
One other alternative to #Jeremiah answer is to define those values in Interface Builder.
I prefer this one because there is no code to add in your ViewController.
Click into your DatePicker view and customise it from the Attribute inspector as in the screenshot.
Works for Swift 2, 3, 4 and probably for Swift < 2. (not tested)
Next solution comes from "arturgrigor" and it works great in my apps, just copy it, paste it in viewDidLoad method, and enjoy it :
[my_datePicker setValue:[UIColor whiteColor] forKeyPath:#"textColor"];
SEL selector = NSSelectorFromString( #"setHighlightsToday:" );
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature :
[UIDatePicker
instanceMethodSignatureForSelector:selector]];
BOOL no = NO;
[invocation setSelector:selector];
[invocation setArgument:&no atIndex:2];
[invocation invokeWithTarget:my_datePicker];
According to Apple's UIKit User Interface Catalog, developers are not allowed to customize date pickers.
I've seen other StackOverflow answers for similar questions that suggest making a fake UIDatePicker using UIPickerView and customizing that.
I also found an open source date picker on GitHub (at https://github.com/mwermuth/MWDatePicker ) that might help a bit. It allows for different background and selector styles, but not a different font or font attributes.... yet.
To change UIDatePicker text color use:
// MARK: - Helping content
private enum DatePickerProperties: String {
case TextColor = "textColor"
case HighlightsToday = "highlightsToday"
}
// MARK: - Outlets
#IBOutlet private weak var datePicker: UIDatePicker!
// MARK: - Lifecicle
public override func awakeFromNib() {
super.awakeFromNib()
self.datePicker.setValue(UIColor.whiteColor(), forKey: DatePickerProperties.TextColor.rawValue)
self.datePicker.setValue(false, forKey: DatePickerProperties.HighlightsToday.rawValue)
}
It works like a charm with xCode 7.3 and Swift 2.3.
I stumbled upon a surprisingly clean solution using UIAppearance, without using any KVC, swizzling, or otherwise private API. I found that attempting to set the textColor via UIAppearance for any UILabel within a UIDatePicker had no affect, but a custom appearance property that simply called the regular textColor setter worked just fine.
// Implement a custom appearance property via a UILabel category
#interface UILabel (PickerLabelTextColor)
#property (nonatomic, strong) UIColor * textColorWorkaround UI_APPEARANCE_SELECTOR;
#end
#implementation UILabel (PickerLabelTextColor)
- (UIColor *)textColorWorkaround {
return self.textColor;
}
- (void)setTextColorWorkaround:(UIColor *)textColor {
self.textColor = textColor;
}
#end
And then use as follows:
UILabel *pickerLabelProxy = [UILabel appearanceWhenContainedInInstancesOfClasses:#[UIDatePicker.class]];
pickerLabelProxy.textColorWorkaround = UIColor.lightGrayColor;
Swift Version
UILabel extension:
extension UILabel {
#objc dynamic var textColorWorkaround: UIColor? {
get {
return textColor
}
set {
textColor = newValue
}
}
}
Appearance proxy use:
let pickerLabelProxy = UILabel.appearance(whenContainedInInstancesOf: [UIDatePicker.self])
pickerLabelProxy.textColorWorkaround = UIColor.lightGray
If anyone wants the swift solution, I placed the following in viewDidLoad:
birthdayDatePicker.setValue(DesignHelper.getOffWhiteColor(), forKey: "textColor")
birthdayDatePicker.performSelector("setHighlightsToday:", withObject:DesignHelper.getOffWhiteColor())
For Xamarin developers:
DatePicker.SetValueForKey(UIColor.White, new NSString("textColor"));
DatePicker.SetValueForKey(FromObject(false), new NSString("highlightsToday"));
It´s working like a charm.
Tested in iOS 9 and 10
You can also add this as an IBDesignable if you want to configure this within InterFace Builder.
import UIKit
#IBDesignable
extension UIDatePicker {
#IBInspectable var textLabelColor: UIColor? {
get {
return self.valueForKey("textColor") as? UIColor
}
set {
self.setValue(newValue, forKey: "textColor")
self.performSelector("setHighlightsToday:", withObject:newValue) //For some reason this line makes the highlighted text appear the same color but can not be changed from textColor.
}
}
}
This subclass of UIDatePicker works for iOS 7. Its not pretty but gets the job done.
#define kNotification_UIView_didAddSubview #"kNotification_UIView_didAddSubview"
#implementation UIView (addSubview)
-(void) didAddSubview:(UIView *)subview{
[[NSNotificationCenter defaultCenter] postNotificationName:kNotification_UIView_didAddSubview object:self];
}
#end
#interface DatePicker ()
#property (nonatomic, strong) UIColor* textColor;
#end
#implementation DatePicker
-(id) initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self){
[self setup];
}
return self;
}
-(id) initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if (self){
[self setup];
}
return self;
}
-(void) setup{
self.textColor = [UIColor darkTextColor];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(subviewsUpdated:) name:kNotification_UIView_didAddSubview object:nil];
}
-(void) dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void) updateLabels:(UIView*) view{
for (UILabel* label in view.subviews){
if ([label isKindOfClass:[UILabel class]]){
label.textColor = self.textColor;
}else{
[self updateLabels:label];
}
}
}
-(BOOL) isSubview:(UIView*) view{
if (view == nil){
return NO;
}
if (view.superview == self){
return YES;
}
return [self isSubview:view.superview];
}
-(void) subviewsUpdated:(NSNotification*) notification{
if ([notification.object isKindOfClass:NSClassFromString(#"UIPickerTableView")] && [self isSubview:notification.object]){
[self updateLabels:notification.object];
}
}
#end
[date_picker setValue:textColor forKey:#"textColor"];
[date_picker performSelector:#selector(setHighlightsToday:) withObject:NO];
Add Runtime Attribute named "textColor" from Storyboard as shown in following image.
As alternative to #Jeremiah answer, you can use this:
datePicker.setValue(UIColor.redColor(), forKey: "textColor")
datePicker.sendAction("setHighlightsToday:", to: nil, forEvent: nil)
datePicker.setDate(NSDate(timeIntervalSinceReferenceDate: 0), animated: false)
it will remove Today (you will see current date), but it will be with right color.
Possible troubles:
if you change color dynamically, I didn't find a way to reload date picker. So, the user will see previous color and only after scroll, color will changed to a new one. -> Solved, by last string. Looks like Voodoo, but it works...
This answer suitable for Swift 2.1
It didn't work until textColor was set inside layoutSubviews()
override func layoutSubviews() {
super.layoutSubviews()
datePicker.backgroundColor = .black
datePicker.setValue(.white, forKeyPath: "textColor")
}
Just use datePicker.tintColor = .red or any other color you want.
[my_date_picker setValue:[UIColor whiteColor] forKey:#"textColor"];
it seems don't work on ios 13 or later, you can set overrideUserInterfaceStyle property for UIDatePicker to determin it shows white color or black.
I am wrapping my head around using categories for some things where I might previously have been using inheritance.
The thing I am doing now is more of a best-practice question where I am not sure how it should be implemented. I am writing a category on UIActivityIndicatorView which basically will be used to put a activity indicator in an arbitrary view. Below you'll find the code example of how I am doing it and my main question is whether or not this is good. And I would appreciate comments on why it's bad if that is the case. Thanks.
The category:
#interface UIActivityIndicatorView (Customizations)
- (UIActivityIndicatorView *) inView:(UIView *) target;
#end
Implementation:
#implementation UIActivityIndicatorView (Customizations)
- (UIActivityIndicatorView *) inView:(UIView *) target {
[self startAnimating];
[self setHidden:NO];
self.frame = target.bounds;
self.backgroundColor = [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.6f];
return self;
}
#end
Then I would use it like this:
[background addSubview:[loader inView:background]];
I am guessing that another way of doing it would be to make an initialization function to take the container view and just return the "styled" view, or perhaps not return anything (void) and just let the method do the styling.
So I am looking for some guidance as to how to handle this.
What kind of worries me is that I am actually making a second copy of the UIActivityIndicatorView which seem unnecessary
No, you don't. You might be confused by the fact that you are returning self from your category method, but that is just a pointer, not the object itself getting copied.
However, I would implement it slightly different:
- (void) addToSuperView:(UIView *) target {
[self startAnimating];
[self setHidden:NO];
self.frame = target.bounds;
self.backgroundColor = [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.6f];
[target addSubview:self];
}
That way, you don't need to make an extra, unnecessary call when adding it:
[loader addToSuperView:background];