10 Tips when moving from Objective-C to Swift

I’m John, one of the iOS engineers of Japan’s popular Couples app.

A year ago, our team started converting our Objective-C code to Swift and we’ve encountered a lot of paradigm changes. Here are 10 things to consider when switching from Objective-C practices.

10. Use Swift’s SequenceType utilities for common array operations

For example, to check if an array is empty or not, use isEmpty instead of checking the length:

Swift Objective-C
if array.isEmpty {
// ...
if (array.length <= 0) {
// ...

When working on indexes, use the indices property instead of operating on the array length.
For example, to iterate all indexes:

Swift Objective-C
for i in array.indices {
    // ...
}
for (NSUInteger i = 0; i < array.length; ++i) {
    // ...
}

To get the first or last index:

Swift Objective-C
let firstIndex = array.indices.first
let lastIndex = array.indices.last
NSUInteger firstIndex = 0;
NSUInteger lastIndex = array.length - 1;

To iterate all indexes except the last n indexes:

Swift Objective-C
for i in array.indices.dropLast(n) {
    // ...
}
for (NSUInteger i = 0; i < (array.length - n); ++i) {
    // ...
}

To iterate all indexes except the first n indexes:

Swift Objective-C
for i in array.indices.dropFirst(n) {
    // ...
}
for (NSUInteger i = n; i < array.length; ++i) {
    // ...
}

Doing so emphasizes the intent and makes code more readable.

9. Use enums with static functions and properties instead of singletons

In Objective-C, we cannot write class-level properties so it was very common to implement shared state as singletons.

In Swift, this is completely unnecessary as types support static vars and static funcs.

Swift Objective-C
final enum GlobalSettings {
    static var isICloudEnabled = false;
    static func saveSettings() {
        // ...
    }
}
@interface GlobalSettings
@property (nonatomic) BOOL isICloudEnabled;
@end

@implementation GlobalSettings

+ (GlobalSettings *)sharedInstance {
    static GlobalSettings *instance;
    static dispatch_once_t once = 0;
    dispatch_once(&once, ^{
        instance = GlobalSettings()
    });
    return instance;
}

- (void)saveSettings {
 // ...
}
@end
if GlobalSettings.isICloudEnabled {
    GlobalSettings.isICloudEnabled = false
    // ...
}
GlobalSettings.saveSettings()
if (GlobalSettings.sharedInstance.isICloudEnabled) {
    GlobalSettings.sharedInstance.isICloudEnabled = NO;
    // ...
}
[GlobalSettings.sharedInstance saveSettings];

Notice that we used enum. Empty structs and classes can be created, while enums cannot. So in effect, they are ideal to use as a fake “namespace” as well as non-initializable singletons.

Swift
struct SettingsStruct {}
class SettingsClass {}
enum SettingsEnum {}

let s = SettingsStruct() // OK
let c = SettingsClass() // OK
let e = SettingsEnum() // Compile error: 'SettingsEnum' cannot be constructed because it has no accessible initializers

8. Use is instead of isKindOfClass()

There’s no advantage to using isKindOfClass: so use the is operator instead and save some typing. The is operator is also more powerful as it also works even on non-class types.

Swift Objective-C
if dictionary["value"] is String {
// ...
if ([dictionary[@"value"] isKindOfClass:[NSString class]]) {
// ...

You can also use is as a condition for switchcase statements.

Swift
switch dictionary["value"] {
case is String: print("It's a string!")
case is Int: print("It's a number!")
case is NSObject: print("It's an object!")
// ...
}

7. We can shadow self just as we did in Objective-C

If you have any experience with Objective-C, you are probably familiar with the weakstrong dance.

Objective-C
typeof(self) __weak weakSelf = self;
[session requestWithCompletion:^{
    typeof(self) __strong self = weakSelf;
    [self showDialog];
}];

Notice that Objective-C lets us declare a variable self inside the block, so we can protect our code from accidentally accessing the weak reference.

In Swift, most people use strongSelf or sSelf or something else, because the compiler won’t let us write:

Swift
session.requestWithCompletion { [weak self] in
    guard let self = self else { // Compile error
        return
    }
    self.showDialog() // Another error, we can only write "self?.showDialog()"
}

But we can actually force variable name shadowing by doing

Swift
session.requestWithCompletion { [weak self] in
    guard let `self` = self else { // OK!
        return
    }
    self.showDialog() // OK! No optional needed!
}

6. Use the Swift version of CGGeometry utilities

In Objective-C, we use global functions and constants declared in CGGeometry.h. In Swift, these are all declared in CGRect, CGSize, and CGPoint extensions.

Swift Objective-C
let width = view.frame.width
let bottom = view.frame.maxY
CGFloat width = CGRectGetWidth(view.frame)
CGFloat right = CGRectGetMaxX(view.frame)

Some constants such as CGRectZero, CGRectNull CGSizeZero, CGPointZero, etc. can be written in Swift as CGRect.zero, CGRect.null, CGSize.zero, CGPoint.zero:

Swift Objective-C
let view = UIView(frame: .zero)
UIView *view = UIView(frame: CGRectZero)

Functions for operating on these structs used to require passing both operands to the function, but with Swift they now work intuitively as member functions:

Swift Objective-C
let isInside = view.bounds.contains(subview.center)
BOOL isInside = CGRectContainsPoint(view.bounds, subview.center)

Checking for equality is also now easy. Before, we had to use functions such as CGSizeEqualToSize(...), now we can simply use Swift’s == operator.

Swift Objective-C
if imageView.bounds.size == image.size {
// ...
if (CGSizeEqualToSize(imageView.bounds.size, image.size)) {
// ...

5. Make use of Swift’s mutable chained properties

In Objective-C, you cannot assign values directly to chained struct fields. In Swift, doing so is perfectly safe so take advantage of it.

Swift Objective-C
self.view.frame.origin.y = 20 // OK!
self.view.frame.origin.y = 20; // This does nothing!!

// You need to do use a temporary variable:
CGRect frame = self.view.frame;
frame.origin.y = 20;
self.view.frame = frame; // OK!

4. You can still scope statements together with do {} blocks

We usually organize our code in Objective-C using braces. We can still do the same in Swift with the do keyword:

Swift Objective-C
do {
    let titleLabel = self.titleLabel
    titleLabel.text = "News"
    titleLabel.textColor = .blueColor()
}
do {
    let subtitleLabel = self.subtitleLabel
    subtitleLabel.text = "2016/2/18"
    subtitleLabel.textColor = .greenColor()
}
{
    UILabel *titleLabel = self.titleLabel;
    titleLabel.text = @"News";
    titleLabel.textColor = [UIColor blueColor];
}
{
    UILabel *subtitleLabel = self.subtitleLabel;
    subtitleLabel.text = @"2016/2/18"
    subtitleLabel.textColor = [UIColor greenColor];
}

Another technique is to use anonymous closures to return values with long initialization:

Swift
lazy var screenImage: UIImage = {

    let largeImage = UIImage(named: "large_image")
    // ...
    let aspectFitImage = // ... resize image
    return aspectFitImage
}()

3. Forget about respondsToSelector: and move to Swift’s Availability API

In Objective-C, we were told to check if a method is usable during runtime with respondsToSelector:. With Swift’s compiler, all methods are checked during compile time, so it is better to use use the #available() API.

Swift Objective-C
let manager = CLLocationManager()
if #available(iOS 9, *) {

    manager.allowsBackgroundLocationUpdates = true
}
CLLocationManager *manager = [CLLocationManager new];
if ([manager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {

    manager.allowsBackgroundLocationUpdates = YES;
}

If you try to use a new API but the minimum targeted iOS version is older and you forget to check with #available(), the compiler will send an error:

Swift
let manager = CLLocationManager()
manager.allowsBackgroundLocationUpdates = true
// Compile error: 'allowsBackgroundLocationUpdates' is only available on iOS 9.0 or newer

2. Take advantage of generic types and protocols instead of concrete types for maximum flexibility

With Swift’s generics, a single declaration can work on many types. As an example, here’s an implementation of the common clamp() function:

Swift Objective-C
func clamp(minValue: T, _ value: T, _ maxValue: T) -> T {
    return min(max(value, minValue), maxValue)
}

let clampedInt: Int = clamp(1, 155, 100) // = 100

let clampedDouble: Double = clamp(1.0, 155.123 , 100.0) // = 100.0
__attribute__((overloadable))
NSInteger clamp(NSInteger min, NSInteger value, NSInteger max) {  
    return MIN(max, MAX(min, value));
}

__attribute__((overloadable))
double clamp(double min, double value, double max) {  
    return MIN(max, MAX(min, value));
}

NSInteger clampedInt = clamp(1, 155, 100); // = 100

double clampedDouble = clamp(1.0, 155.123, 100.0); // 100.0

The generic method already works on all numeric types, while in Objective-C we’ll need to declare an overload for all needed types.

1. Master Swift’s pattern matching

If there’s one Swift function that I like most, it’s pattern matching. if statements, switch/case statements, guard conditions, for, while and repeat loops, and even do/catch handlers all support pattern matching.

One place I like using pattern matching on is in prepareForSegue(_:):

Swift Objective-C
switch (segue.destinationViewController, segue.identifier) {
    
case (let controller as SomeViewController, _):
    // ... do something
    
case (let controller as AnotherViewController, "SomeIdentifier"?):
    // ... do something
    
case (let controller as AnotherViewController, "AnotherIdentifier"?):
    // ... do something
    
default:
    break
}
if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]) {

    // ... do something
}
else if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]
    && [segue.identifier isEqualToString:@"SomeIdentifier"]) {
    
    // ... do something
}
else if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]
    && [segue.identifier isEqualToString:@"AnotherIdentifier"]) {
    
    // ... do something
}

The case statement is very powerful, and works with other control flow statements as well:

Swift
if case (let controller as AnotherViewController) = segue.controller where segue.identifier == "SomeIdentifier" {

    // ... do something
}
// skips all subviews that are not UIButton
for case (let button as UIButton) in self.subviews {

    // ... do something
}
if case (let controller as AnotherViewController) = segue.controller where segue.identifier == "SomeIdentifier" {

    // ... do something
}
do {

    try removeDatabase(path)
}
catch MyCustomError.Cancelled {

    cancellation()
}
catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError {

    // ignore
    completion()
}
catch let error as NSError {

    failure(error)
}

Once you get used to using pattern matching, coding logic becomes clearer and simpler.

Conclusion

While Apple made the transition from Objective-C to Swift very smooth, moving to a different language means throwing out old practices and adopting new ones best for the language. Take advantage of the new API and techniques offered by Swift!

We also published eureka’s Swift Style Guide so do check it out!

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

Go Conference 2016 Springで発表してきました

RaspberryPi×Go言語で電子工作