125
votes

I'm trying to convert my app to the Swift language.

I have this line of code:

[[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], nil]
                     setTitleTextAttributes:textDictionary
                                   forState:UIControlStateNormal];

How to convert it to Swift?

In Apple's docs, there is no such method.

11
@LukeTheObscure check out my answer below... ugly, but works.tcd

11 Answers

231
votes

Update for iOS 9:

If you're targeting iOS 9+ (as of Xcode 7 b1), there is a new method in the UIAppearance protocol which does not use varargs:

static func appearanceWhenContainedInInstancesOfClasses(containerTypes: [AnyObject.Type]) -> Self

Which can be used like so:

UITextField.appearanceWhenContainedInInstancesOfClasses([MyViewController.self]).keyboardAppearance = .Light

If you still need to support iOS 8 or earlier, use the following original answer to this question.

For iOS 8 & 7:

These methods are not available to Swift because Obj-C varargs methods are not compatible with Swift (see http://www.openradar.me/17302764).

I wrote a non-variadic workaround which works in Swift (I repeated the same method for UIBarItem, which doesn't descend from UIView):

// UIAppearance+Swift.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIView (UIViewAppearance_Swift)
// appearanceWhenContainedIn: is not available in Swift. This fixes that.
+ (instancetype)my_appearanceWhenContainedIn:(Class<UIAppearanceContainer>)containerClass;
@end
NS_ASSUME_NONNULL_END

// UIAppearance+Swift.m
#import "UIAppearance+Swift.h"
@implementation UIView (UIViewAppearance_Swift)
+ (instancetype)my_appearanceWhenContainedIn:(Class<UIAppearanceContainer>)containerClass {
    return [self appearanceWhenContainedIn:containerClass, nil];
}
@end

Just be sure to #import "UIAppearance+Swift.h" in your bridging header.

Then, to call from Swift (for example):

# Swift 2.x:
UITextField.my_appearanceWhenContainedIn(MyViewController.self).keyboardAppearance = .Light

# Swift 3.x:
UITextField.my_appearanceWhenContained(in: MyViewController.self).keyboardAppearance = .light
32
votes

ios 10 swift 3

UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).title = "Kapat"
14
votes

For iOS 8 & 7:

I use a category based on Alex's answer to specify multiple containers. This is a workaround until Apple officially supports appearanceWhenContainedIn in Swift.

UIAppearance+Swift.h

@interface UIView (UIAppearance_Swift)
/// @param containers An array of Class<UIAppearanceContainer>
+ (instancetype)appearanceWhenContainedWithin: (NSArray *)containers;
@end

UIAppearance+Swift.m

@implementation UIView (UIAppearance_Swift)

+ (instancetype)appearanceWhenContainedWithin: (NSArray *)containers
{
    NSUInteger count = containers.count;
    NSAssert(count <= 10, @"The count of containers greater than 10 is not supported.");
    
    return [self appearanceWhenContainedIn:
            count > 0 ? containers[0] : nil,
            count > 1 ? containers[1] : nil,
            count > 2 ? containers[2] : nil,
            count > 3 ? containers[3] : nil,
            count > 4 ? containers[4] : nil,
            count > 5 ? containers[5] : nil,
            count > 6 ? containers[6] : nil,
            count > 7 ? containers[7] : nil,
            count > 8 ? containers[8] : nil,
            count > 9 ? containers[9] : nil,
            nil];
}
@end

Then add #import "UIAppearance+Swift.h" to your bridging header.

To use from Swift:

TextField.appearanceWhenContainedWithin([MyViewController.self, TableViewController.self]).keyboardAppearance = .Light

It was good if I could find a way using CVarArgType, but I found no clean solution.

12
votes

Here's a less ugly, but still ugly, workaround inspired by @tdun.

  1. Create a class to hold your Objective-C appearance. For the purposes of this example, let's call it AppearanceBridger.
  2. Add this class to your bridging header. If you don't have a bridging header, create one.
  3. Create a class method in AppearanceBridger named +(void)setAppearance and put the Objective-C appearance code in this method. For example:


+ (void)setAppearance {
    [[UIView appearanceWhenContainedIn:[UITableViewHeaderFooterView class], nil] setBackgroundColor:[UIColor whiteColor]];
}
  1. In your Swift code where you set the appearance, call AppearanceBridger.setAppearance() and you should be good to go!

Hope this works well for people who see it.

4
votes

Here's an ugly workaround solution I used....

Just make an Objective-C Cocoa Touch Class (UIViewController), named whatever you want.

I named mine WorkaroundViewController...

Now in (WorkaroundViewController.m):

-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil

Run the Objective-C appearance code for .appearanceWhenContainedIn() (here's my example):

[[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setDefaultTextAttributes:@{NSFontAttributeName: [UIFont fontWithName:@"Avenir-Light" size:16.0f]}];

Then create a bridging header for your Swift project and then initialize your Objective-C ViewController in your Swift code, like this (again, just my example):

var work : WorkaroundViewController = WorkaroundViewController()

Then you're done! Let me know if it works for you... Like I said, it's ugly, but works!

4
votes

This can be extended to any class that conforms to the UIAppearance protocol -- not just UIViews. So here's a more generic version:

UIAppearance+Swift.h

#import <UIKit/UIKit.h>

@interface NSObject (UIAppearance_Swift)

+ (instancetype)appearanceWhenContainedWithin:(Class<UIAppearanceContainer>)containerClass;

@end

UIAppearance+Swift.m

#import "UIAppearance+Swift.h"

@implementation NSObject (UIAppearance_Swift)

+ (instancetype)appearanceWhenContainedWithin:(Class<UIAppearanceContainer>)containerClass {
    if ([self conformsToProtocol:@protocol(UIAppearance)]) {
        return [(id<UIAppearance>)self appearanceWhenContainedIn:containerClass, nil];
    }
    return nil;
}

@end
3
votes

I have created a repo for you guys who wanna use CocoaPods:

  1. Add this into your Podfile:

    pod 'UIViewAppearanceSwift'
    
  2. Import in your class:

    import UIViewAppearanceSwift
    
    func layout() {
        UINavigationBar.appearanceWhenContainedWithin(MFMailComposeViewController.self).barStyle = .Black
        UIBarButtonItem.appearanceWhenContainedWithin(UISearchBar.self).setTitleTextAttributes([NSFontAttributeName: UIFont.systemFontOfSize(15)], forState: UIControlState.Normal)
    }
    
  3. Reference: https://github.com/levantAJ/UIViewAppearanceSwift

1
votes

Swift 4: iOS 9+

UIProgressView.appearance(whenContainedInInstancesOf: [LNPopupBar.self]).tintColor = .red
0
votes

It seems Swift (at least as of Beta5) isn't able to support it for reasons unknown to me. Perhaps the language feature required is still in progress, as I can only assume they left it out of the interface for a good reason. Like you said, according to the docs it's still available in ObjC. Really disappointing.

-3
votes

You can use this:

UIBarButtonItem.appearance().setTitleTextAttributes(textDictionary, forState: UIControlState.Normal)

Edit: appearanceWhenContainedIn was removed in Swift. This answer was for the Beta 5 to change the appearance of the text of all bar buttons.

-7
votes

You should be able to just translate the Objective-C syntax into Swift syntax.

In swift the methods should be declared like this:

func appearanceWhenContainedIn(containerClass : <UIAppearanceContainer>)
func setTitleTextAttributes(_ attributes: NSDictionary!, forState state: UIControlState)

So you can try this:

UIBarButtonItem.appearanceWhenContainedIn(UINavigationBar).setTitleTextAttributes(textDictionary, forState: UIControlStateNormal)

I still have to figure out if this is the clean way to call a class method in Swift though.

Hope this helps,