Migrating to Swift
The earliest date within my iOS app Herd is May 3rd 2009. Thanks to the iOSDevUK Conference and its attendees for the moral support to migrate almost wholesale to Swift. These are my ongoing notes chronicling this migration.
- General outline
- Day 1 (Saturday 8th)
- Day 2 (Monday 10th)
- Day 3 (Tuesday 11th)
- Day 4 (Wednesday 12th)
- Day 5 (Thursday 13th)
- Day 6 (Monday 17th)
- Day 7 (Tuesday 18th)
- Day 8 (Wednesday 19th)
- Final Thoughts
- Appendix - Links
General outline
If, like me you’re trying to make money out of your app then you’ll be looking to perform a staged migration from Objective-C to Swift. At a high level your best strategy to perform your migration in these steps
- Clean up your Objective-C code to adhere to the latest standards
- Migrate your Objective-C files to Swift in the current Objective-C dialect (ie approximately zero refactoring)
- Clean up your Swift to adhere to the latest standards
Step 2 is critical. I’ve performed quite a few code migrations over the years and one guarenteed way to fail is to change architecture and language at the same time. Do one, then the other; do not perform them simultaneously.
Day 1 (Saturday 8th)
Upgraded to the latest Xcode build settings
My first step was to ensure the project contained zero warnings by enabling the build setting treat warnings as errors
. I then upgraded the build settings. This produced more errors (warnings are errors now), which were then fixed.
Updated the deployment target from 8.1 to 9.3
Increasing the minimum deployment target brought with it a whole bunch of deprecation errors (eg UIActionSheet
and UIAlert
).
However, this also represented a good opportunity to test my first piece of Swift code - a Swift extension of UIAlertController
. Since all my alerts are of the form “something went wrong and there’s no action to take” the replacement is pretty simple:
import UIKit
extension UIAlertController
{
@objc public convenience init(title: String?, message: String?, delegate: UIAlertViewDelegate?, cancelButtonTitle: String?, otherButtonTitles firstButtonTitle: String?)
{
assert(delegate == nil)
self.init(title: title, message: message, preferredStyle: .alert)
self.addAction(UIAlertAction(title: cancelButtonTitle, style: .cancel, handler: nil))
}
@objc func show()
{
UIApplication.shared.delegate?.window??.rootViewController?.present(self, animated: true, completion: nil)
}
}
Note the abomination that was window??`. I’m not going to fix that simply because it’s ugly - this code is already a hack, it will be removed in later phase of the migration.
Populated the bridging header
Adding the UIAlertController
extension created both Herd-Bridging-Header.h
and Herd-Swift.h
. I could then start the next sensible step: within .m
files I moved all #import
statments to Herd-Bridging-Header.h
then added back only two import statements
#import "Herd-Swift.h"
#import "Herd-Bridging-Header.h"
This meant that all Objective-C code could access Swift code, and all Swift code could access Objective-C code. This will be useful as I migrate more files across.
Day 2 (Monday 10th)
Migrating the first Objective-C class
Painful experience has taught me that manual translation is too prone to errors - humans just screw this stuff up too much, and too slow.
These are the tools I found
- Convert Objective-C to Swift Online | iSwift
- objc2swift - The Open Source Obj-C to Swift Converter (GitHub)
- Swiftify | Objective-C to Swift Converter
Of those, Swiftify appeared to give me the best results. The class I chose was one of the simplest and was pretty succesful. This was my process
- Remove the
.h
file fromHerd-Bridging-Header.h
. - Remove the
.m
file from the target in Xcode. - Create a
.swift
file of the same name, and add it to the target. Open a new window in my favour editor (BBEdit) and paste the.h
file followed by the.m
file. - Copy the contents into Swiftify’s converter window in Chrome and hit
CONVERT NOW!
. - Copy the converted Swift into the
.swift
file and compile. It will likely fail.
Chances were that small edits to the generated Swift would make everything work. However I sometimes found that editing the original Objective-C, then rerunning the conversion gave me better results more quickly.
Day 3 (Tuesday 11th)
Lazily initialized properties
My next class had lots of lazily initialized properties of the form
@interface DateField : UIControl
@property (strong, nonatomic) UIBarButtonItem * _Nonnull datePickerTypeButton;
@end
@implementation DateField
-(nonnull UIBarButtonItem *)datePickerTypeButton
{
if (!_datePickerTypeButton)
{
_datePickerTypeButton =
[[UIBarButtonItem alloc] initWithImage:nil style:UIBarButtonItemStylePlain target:self action:@selector(toggleDatePickerType)];
}
return _datePickerTypeButton;
}
@end
None of the conversion tools gave me what I needed. It was a bit of a struggle until I found Lazy Initialization with Swift by Mike Buss.
@interface DateField : UIControl
@property (strong, nonatomic) UIBarButtonItem * _Nonnull datePickerTypeButton;
@end
@implementation DateField
-(nonnull UIBarButtonItem *)datePickerTypeButton
{
if (_datePickerTypeButton == nil)
{
_datePickerTypeButton = [self defaultDatePickerTypeButton];
}
return _datePickerTypeButton;
}
-(nonnull UIBarButtonItem *)defaultDatePickerTypeButton
{
UIBarButtonItem *datePickerTypeButton = [[UIBarButtonItem alloc] initWithImage:nil style:UIBarButtonItemStylePlain target:self action:@selector(toggleDatePickerType)];
return datePickerTypeButton;
}
@end
Using Swiftify will produce something like this, which will break
class DateField: UIControl {
private var _datePickerTypeButton: UIBarButtonItem!
var datePickerTypeButton: UIBarButtonItem {
if _datePickerTypeButton == nil {
_datePickerTypeButton = defaultDatePickerTypeButton()
}
return _datePickerTypeButton
}
func defaultDatePickerTypeButton() -> UIBarButtonItem {
let datePickerTypeButton = UIBarButtonItem(image: nil, style: .plain, target: self, action: #selector(DateField.toggleDatePickerType))
return datePickerTypeButton
}
}
The final step is to switch datePickerTypeButton
to a native Swift lazily initialized property
class DateField: UIControl {
lazy var datePickerTypeButton: UIBarButtonItem = defaultDatePickerTypeButton()
func defaultDatePickerTypeButton() -> UIBarButtonItem {
let datePickerTypeButton = UIBarButtonItem(image: nil, style: .plain, target: self, action: #selector(DateField.toggleDatePickerType))
return datePickerTypeButton
}
}
This step technically breaks my no refactoring rule, but I just couldn’t help myself. If I stick to the above pattern then a regex later in the migration should clean up all such instances.
However, in the days between discovering the problem and writing it up, the issue has been fixed without me raising a bug (because I didn’t think the tool was actually wrong). It’s nice to see the tool is actively being updated.
Day 4 (Wednesday 12th)
Non-standard Objective-C
It turned out that I’d already changed my Objective-C to look more like Swift
if ([response isKindOfClass:NSHTTPURLResponse.self]) { ... }
Swiftify did not like the call to self
, and to be fair, why would it. I had to replace all instances of .self
with .class
to get nice clean output.
Overriding variables
Similar to day 4’s Lazily initialized properties issue, I had to make a choice whether to break my no-refactoring rule. As implied earlier, Swift and Objective-C handle object properties very differently
-(void)setEnabled:(BOOL)enabled
{
[super setEnabled:editing];
if (!enabled && [self isFirstResponder])
{
[self resignFirstResponder];
}
}
translated to this
func setEnabled(_ enabled: Bool) {
super.enabled = editing
if !enabled && isFirstResponder {
resignFirstResponder()
}
}
Which will either fail to compile, or compile and function incorrectly. I wasn’t sure what to do so I layed out a few options
override var enabled : Bool {
set {
super.enabled = editing
if !enabled && isFirstResponder {
resignFirstResponder()
}
}
get {
return super.enabled
}
}
(Swift requires that if I use set
I also have to supply a get)
override var enabled : Bool {
didSet {
if !enabled && isFirstResponder {
resignFirstResponder()
}
}
}
This is cleaner, but I think maybe I should have edited the Objective-C first to something like this
-(void)setEnabled:(BOOL)enabled
{
BOOL oldValue = self.enabled;
[super setEnabled:editing];
[self didSetEnabled:oldValue];
}
-(void)didSetEnabled:(BOOL)oldValue
{
if (!self.enabled && [self isFirstResponder])
{
[self resignFirstResponder];
}
}
which translates to
override var enabled : Bool {
didSet { didSetEnabled(oldValue) }
}
func didSetEnabled(_ oldValue: Bool) {
if !enabled && isFirstResponder {
resignFirstResponder()
}
}
This is longer, but it might be worth considering if the content of didSetEnabled
is much longer.
Day 5 (Thursday 13th)
Variadic function is unavailable
I had a few variadic logging functions of the form
void croak(NSString * _Nonnull message, ...) NS_FORMAT_FUNCTION(1,2);
which aren’t available in Swift. The simplest wrapping would have the signature
croak(_ message: String, _ args: CVarArg?...) -> Never
but that didn’t really work. Using NSLog()
every day we often forget just what evil type mangling these functions do. Alas, strong type handling is a big thing in Swift, and the work invloved to harmonise the two would probably require a pure Swift implementation of the backing function to all these calls __CFStringAppendFormatCore:
static void __CFStringAppendFormatCore(CFMutableStringRef outputString, CFStringRef (*copyDescFunc)(void *, const void *), CFStringRef (*contextDescFunc)(void *, const void *, const void *, bool, bool *), CFDictionaryRef formatOptions, CFDictionaryRef stringsDictConfig, CFStringRef formatString, CFIndex initialArgPosition, const void *origValues, CFIndex originalValuesSize, va_list args)
{
// 579 lines of crazy multiplatform C
}
That’s a large project in of itself. So I ended up admiting defeat by using Swift’s String Interpolation and reducing the function signature to:
croak(_ message: String) -> Never
Day 6 (Monday 17th)
Starting the Core Data migration
The problem I found very quickly is that I’d used subentities so I had a large hierarchy of classes to deal with. Swift doesn’t allow an Objective-C class to inherit from a Swift class so this was going to be an arduous task of slowly moving one class at a time, starting with the leafs and working my way up the super classes.
The first thing I did was to disable mogenerator’s Objective-C class generation, then slowly migrate each of the generated class one by one.
It took days.
Day 7 (Tuesday 18th)
Garbage in, garbage out
This started out as a simple conversion which failed to compile. At 29 lines, this method didn’t seem too long. In the end this was a trainwreck of a method.
+ (instancetype _Nonnull)ensureWithProperties:(NSDictionary <NSString* , NSObject* >* _Nonnull)properties inManagedObjectContext:(NSManagedObjectContext* _Nonnull)moc_
{
NSMutableArray *predicates = [NSMutableArray array];
for (NSString *name in [properties allKeys])
{
NSString *format = [[@"(" stringByAppendingString:name] stringByAppendingString:@" == %@)"];
[predicates addObject:[NSPredicate predicateWithFormat:format, [properties objectForKey:name]]];
}
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:[self entityName]];
[fetchRequest setPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:predicates]];
NSError *error = nil;
NSArray *objects = [moc_ executeFetchRequest:fetchRequest error:&error];
if ([objects count] > 0)
{
return [objects objectAtIndex:0];
}
else
{
_MO *object = [self insertInManagedObjectContext:moc_];
for (NSString *name in [properties allKeys])
{
[object setValue:[properties objectForKey:name] forKey:name];
}
return object;
}
}
Putting it through the tranlator created output that was effectively junk. The tool often produces small issues, but this was just broken.
In hindsight I should have seen it before hand; the method is actually performing quite a few tasks, some of them not quite in the spirit of Obective-C.
+ (instancetype _Nullable)fetchWithProperties:(NSDictionary <NSString* , NSObject* >* _Nonnull)properties inManagedObjectContext:(NSManagedObjectContext* _Nonnull)moc_
{
NSMutableArray *predicates = [NSMutableArray array];
for (NSString *name in [properties allKeys])
{
NSString *format = [[@"(" stringByAppendingString:name] stringByAppendingString:@" == %@)"];
[predicates addObject:[NSPredicate predicateWithFormat:format, [properties objectForKey:name]]];
}
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:[self entityName]];
[fetchRequest setPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:predicates]];
NSError *error = nil;
NSArray *objects = [moc_ executeFetchRequest:fetchRequest error:&error];
return objects.firstObject;
}
+ (instancetype _Nonnull)ensureWithProperties:(NSDictionary <NSString* , NSObject* >* _Nonnull)properties inManagedObjectContext:(NSManagedObjectContext* _Nonnull)moc_
{
_MO * object = [self fetchWithProperties:properties inManagedObjectContext:moc_];
if (!object)
{
object = [self insertInManagedObjectContext:moc_];
for (NSString *name in [properties allKeys])
{
[object setValue:[properties objectForKey:name] forKey:name];
}
}
return object;
}
Admittedly, that wasn’t much better, but it was enough to make the translation more managable.
Self as a return type
Another issue I faced with the same set of methods was that instancetype
doesn’t appear translate well. Nominally, it translates to Self
, but is far more restricted.
Luckily the accepted answer on Stack Overflow to how can I create instances of managed object subclasses in a NSManagedObject Swift extension? was excellent and introduced me to unsafeDowncast(_:to:)
.
unsafeDowncast is just the kind of function I worried Swift would not have; a get out of jail at a very high price card. The documentation has this to say
This function trades safety for performance. Use
unsafeDowncast(_:to:)
only when you are confident that x is T always evaluates to true, and only after x as! T has proven to be a performance problem.
My painful experience about using such functions is that they should only be used in utility code, never be used in general application code.
Day 8 (Wednesday 19th)
Categories on Foundation classes
I had a category on NSUUID which I needed to use in Swift.
@interface NSUUID (NAC)
@property (nonatomic, readonly) NSString *NAC;
@end
Alas, Swift uses UUID with toll-free briding, but not for categories. There are two ways to solve this: convert the whole category to Swift (which you may not wish to do yet), or create a small stub extension on UUID (which is what I did)
extension UUID {
var nac : String {
return (self as NSUUID).nac
}
}
Final Thoughts
To summarize, this project would have been a failure if I’d tried to redesign the app while migrating. I know this because this is the 3rd attempt. Both previous projects started as app redesigns, and both failed.
Just follow the rules at the top, and use an automated tool like Swiftify. You’ll need to make fixes, but they’ll mostly be just that - fixes.
Appendix - Links
Articles
- How Do I Declare a Closure in Swift?
- Migrating an Objective-C class to Swift using subclassing
- Migrating From Objective-C to Swift
- Migrating Your Objective-C Code to Swift | Apple Developer Documentation
- Migrating Your Objective-C Programs to Swift – A Simplified Guide for iOS Developers
- Migrating Your Objective-C Project to Swift – Swiftify – Medium
- Better Core Data models in Swift
- Why Swift Is The Most Satisfying Language For Using Core Data