`self = x` in NSObject initializers

Please, is there a better way to implement an async initializer where the actual construction is external to the call?

private protocol NSOtherable { }

extension NSObject: NSOtherable { }

extension NSOtherable where Self: NSObject {
    init(other: Self) {
        self.init(__other: other)
    }
}

extension UIImage {
    convenience init(url: URL?) async throws {
        try await self.init(other: Self.load(url: url) as! Self)
    }
    
    private static func load(url: URL?) async throws -> UIImage {
        // ...
    }
}

@interface NSObject (Other)

- (instancetype)initWithOther:(NSObject*)other NS_REFINED_FOR_SWIFT;

@end

@implementation NSObject (Other)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"

- (instancetype)initWithOther:(NSObject*)other {
    if (self.class != other.class) {
        [NSException raise:@"Incorrect class" format:@"'%@' can't be passed as '%@'", NSStringFromClass(other.class), NSStringFromClass(self.class)];
    }
    return other;
}

#pragma clang diagnostic pop

@end

As an aside, extending NSObjectProtocol insead of creating NSOtherable causes a compule fail:

extension NSObjectProtocol where Self: NSObject {
    init(other: Self) {
        self.init(__other: other)
    }
}

// Undefined symbols for architecture arm64:
//   "(extension in AsyncInit):__C.NSObject< where A: __C.NSObject>.init(foo: A) -> A", referenced from:
//       (2) suspend resume partial function for (extension in AsyncInit):__C.UIImage.init(url: Foundation.URL?) async throws -> __C.UIImage in UIImage+Extensions.o
// ld: symbol(s) not found for architecture arm64
// clang: error: linker command failed with exit code 1 (use -v to see invocation)

Is this specific to UIImage, or are you using UIImage as an illustrative example only?

I must admit the way you do it is quite ingenious. However, can't you use a static function directly?

try await UIImage.image(with: url)

FWIW, there are quite a few UIImage creation API's that are not "inits": imageNamed, systemImageNamed, animatedImageNamed, etc. and it doesn't seem to be a problem in real world apps.


If you can only stomach this "canonic" form of image creation: try await UIImage(url: url) then consider another ingenious trick that would allow just that:

func UIImage(url: URL) async throws -> UIImage { ... }

For completeness, there might be other ways that go through some intermediate representation (e.g. CGImage -> UIImage, or Data -> UIImage) but they have their limitations and performance issues.


NB. This is how obj-c imageNamed static method is translated into swift's "init":

UIKit.apinotes:
...
- Name: UIImage
  Methods:
  - Selector: 'imageNamed:'
    MethodKind: Class
    SwiftName: 'init(named:withConfiguration:)'
1 Like

I really appreciate you taking the time to make a wonderful enumeration of what’s available.

I’m honestly not sure why I wanted an init based solution, maybe I just thought it was cooler or something. Also I’m an old Objective-C guy so was more than happy to go down the rabbit hole.

BTW, your ingenious trick works without going to Obj-C:

protocol Otherable { // what a name...
    init(other: Self)
}

extension Otherable {
    init(other: Self) { self = other } // 😂
}

extension UIImage: Otherable {
    enum Err: Error { case imageConversionError }
    convenience init(url: URL) async throws {
        let image = try await Self.load(url: url)
        self.init(other: image as! Self)
    }
    private static func load(url: URL) async throws -> UIImage {
        let (data, _) = try await URLSession.shared.data(from: url)
        guard let image = UIImage(data: data) else {
            throw UIImage.Err.imageConversionError
        }
        return image
    }
}
2 Likes