4
\$\begingroup\$

I am converting an old Objective-C class into Swift. My actual question is at the very end after all of the code.

Here is a cut-down version of the Objective-C class:

DateInfo.h:

#import <Foundation/Foundation.h>

@interface RMYearInfo : NSObject

// There are also some instance properties but those aren't relevant to the question

+ (NSInteger)numberOfMonths;
+ (NSArray *)shortMonthNames;
+ (NSArray *)longMonthNames;
// several other class methods

@end

DateInfo.m:

#import "DateInfo.h"

static NSArray *shortMonthNames = nil;
static NSArray *longMonthNames = nil;
// there are several other statics as well

@implementation DateInfo

+ (void)reinitialize {
    NSCalendar *cal = [NSCalendar currentCalendar];

    NSDateFormatter *yearFormatter = [[NSDateFormatter alloc] init];

    shortMonthNames = [yearFormatter shortStandaloneMonthSymbols];
    longMonthNames = [yearFormatter standaloneMonthSymbols];

    // lots of other processing for the other statics
}

+ (void)initialize {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reinitialize) name:NSCurrentLocaleDidChangeNotification object:nil];

    [self reinitialize];
}

+ (NSInteger)numberOfMonths {
    return shortMonthNames.count;
}

+ (NSArray *)shortMonthNames {
    return shortMonthNames;
}

+ (NSArray *)longMonthNames {
    return longMonthNames;
}

// Lots of other instance and class methods

As you can see, the initialize method sets up a notification handler so all of the static variables can be reinitialized if the locale is changed while the app is running.

Here is my Swift code. Since there is no initialize in Swift (any more), my solution is to use private backing variables for the public static variables.

import Foundation

public struct DateInfo {
    // some normal instance properties irrelevant to the question

    private static var _formatter: DateFormatter!
    private static var formatter: DateFormatter {
        if _formatter == nil {
            _formatter = DateFormatter()
            NotificationCenter.default.addObserver(forName: NSLocale.currentLocaleDidChangeNotification, object: nil, queue: nil) { (notification) in
                _formatter = DateFormatter()
                _shortMonthNames = nil
                _longMonthNames = nil
                // reset all of the other statics as well
            }
        }

        return _formatter
    }

    private static var _shortMonthNames: [String]!
    public static var shortMonthNames: [String] {
        if _shortMonthNames == nil {
            _shortMonthNames = formatter.shortStandaloneMonthSymbols
        }

        return _shortMonthNames
    }

    private static var _longMonthNames: [String]!
    public static var longMonthNames: [String] {
        if _longMonthNames == nil {
            _longMonthNames = formatter.standaloneMonthSymbols
        }

        return _longMonthNames
    }

    public static var numberOfMonths: Int {
        return shortMonthNames.count
    }

    // lots of other similar private/public pairs of statics
}

Is this an appropriate way to translate the functionality given the need to be able to reinitialize the statics? I don't like having a private static backing each public static property.

\$\endgroup\$
  • \$\begingroup\$ There is an init in swift: docs.swift.org/swift-book/LanguageGuide/Initialization.html \$\endgroup\$ – muescha May 6 at 12:19
  • \$\begingroup\$ @muescha I'm not referring to init. I'm referring to the Objective-C class method initialize which Swift also had until Swift 3 (I think that's when it went away). \$\endgroup\$ – rmaddy May 6 at 16:16
  • \$\begingroup\$ I don't find this objective-c method with Google in older swift releases \$\endgroup\$ – muescha May 7 at 17:20
  • \$\begingroup\$ i just have no idea - can you help me: what is so different between the initialize and the init? Why you like to call an objective-c method in a pure swift implementation... \$\endgroup\$ – muescha May 7 at 17:22
  • \$\begingroup\$ @muescha See NSObject +load and +initialize - What do they do? for an explanation of what the Objective-C initialize method does. See Swift 3.1 deprecates initialize(). How can I achieve the same thing? for a discussion about Swift eliminating the same method. \$\endgroup\$ – rmaddy May 8 at 0:36
3
\$\begingroup\$

Some thoughts:

  • Make formatter a static stored property (which are guaranteed to be lazily initialized only once). This allows to get rid of the backing property _formatter.
  • For more clarity, move the reinitialization code to a separate method, as in your Objective-C version.
  • Do not cache the other static properties. For example, returning formatter.shortStandaloneMonthSymbols is only one indirection more than returning _shortMonthNames, but is simpler and allows to get rid of the remaining backing properties.
  • A minor point: The notification closure does not access the (notification) argument, which can therefore be replaced by _.

Putting it together, we have the following implementation:

public struct DateInfo {

    private static func reinitialize() {
        formatter = DateFormatter()
    }

    private static var formatter: DateFormatter = {
        // This closure is executed exactly once, on the first accesss of the `formatter` property.
        NotificationCenter.default.addObserver(forName: NSLocale.currentLocaleDidChangeNotification, object: nil, queue: nil) { _ in
            reinitialize()
        }
        return DateFormatter()
    }()

    public static var shortMonthNames: [String] {
        return formatter.shortStandaloneMonthSymbols
    }

    public static var longMonthNames: [String] {
        return formatter.standaloneMonthSymbols
    }

    public static var numberOfMonths: Int {
        return shortMonthNames.count
    }
}

Another option would be to use the typical Singleton pattern:

public class DateInfo {
    static let shared = DateInfo()

    private var formatter: DateFormatter

    private init() {
        formatter = DateFormatter()
        NotificationCenter.default.addObserver(forName: NSLocale.currentLocaleDidChangeNotification, object: nil, queue: nil) { _ in
            self.formatter = DateFormatter()
        }
    }

    public var shortMonthNames: [String] {
        return formatter.shortStandaloneMonthSymbols
    }

    public var longMonthNames: [String] {
        return formatter.standaloneMonthSymbols
    }

    public var numberOfMonths: Int {
        return shortMonthNames.count
    }
}

The advantage is that all initialization is clearly done in the init method. A small disadvantage might be that more typing is needed to access the properties (e.g. DateInfo.shared.shortMonthNames).

\$\endgroup\$
3
\$\begingroup\$

A few observations:

  • I might suggest that we want to use stored properties, like the original Objective-C code. I would be wary of using computed properties that return collections, as that can introduce non-obvious performance hits if you reference this computed property repeatedly, causing the whole array to be re-retrieved multiple times. Admittedly, this collection is small enough, it’s unlikely to be material, but it is something to be sensitive to when dealing with computed properties and collections.

  • I see no reason to store the DateFormatter. If you are using it for other purposes, then go ahead and do that, but there is nothing in this example that suggests that is the case.

  • I’d personally go towards a singleton, too

Thus, perhaps something like:

class DateInfo {
    static let shared = DateInfo()

    private(set) var shortMonthNames: [String] = []
    private(set) var longMonthNames: [String] = []
    private(set) var numberOfMonths: Int = 0

    private init() {
        NotificationCenter.default.addObserver(forName: NSLocale.currentLocaleDidChangeNotification, object: nil, queue: nil) { [weak self] _ in
            self?.update()
        }
        update()
    }

    private func update() {
        let formatter = DateFormatter()
        shortMonthNames = formatter.shortStandaloneMonthSymbols
        longMonthNames = formatter.standaloneMonthSymbols
        numberOfMonths = shortMonthNames.count
    }
}

And, if you have view controllers that are also observing .currentLocaleDidChangeNotification, you might want to eliminate any race conditions by introducing your own notification, e.g. .dateInfoChanged:

extension Notification.Name {
    static let dateInfoChanged = Notification.Name(rawValue: Bundle.main.bundleIdentifier! + ".dateInfoChanged")
}

And then:

class DateInfo {
    static let shared = DateInfo()

    private(set) var shortMonthNames: [String] = []
    private(set) var longMonthNames: [String] = []
    private(set) var numberOfMonths: Int = 0

    private init() {
        NotificationCenter.default.addObserver(forName: NSLocale.currentLocaleDidChangeNotification, object: nil, queue: nil) { [weak self] _ in
            self?.update()
            NotificationCenter.default.post(name: .dateInfoChanged, object: nil)
        }
        update()
    }

    private func update() {
        let formatter = DateFormatter()
        shortMonthNames = formatter.shortStandaloneMonthSymbols
        longMonthNames = formatter.standaloneMonthSymbols
        numberOfMonths = shortMonthNames.count
    }
}

Then view controllers can observe .dateInfoChanged, and you’ll be confident that they’ll be getting this month info after it was updated.

\$\endgroup\$
  • 1
    \$\begingroup\$ I like the suggestion about the custom notification. I haven't gotten far enough to actually test the locale changes yet but I can clearly see how this suggestion would be useful. \$\endgroup\$ – rmaddy May 15 at 19:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.