Working with Foundation types doesn't seem to work as documented

In the Overview section of the article on Working with Foundation Types, it says...

When importing the Foundation framework, the Swift overlay provides value types for many bridged reference types. Many other types are renamed or nested to clarify relationships.

In the next section, it says...

The value types in the table below have the same functionality as their corresponding reference types.

Below the table, it says (my emphasis)...

When Swift code imports Objective-C APIs, the importer replaces Foundation reference types with their corresponding value types. For this reason, you should almost never need to use a bridged reference type directly in your own code.

The table includes String, which corresponds to NSString. I understood this to imply that import Foundation would effectively extend String so it's functionally interchangeable with NSString.

In my code I have this function, which contains two references to NSString:

func readfile(directory: String, filename: String) -> String {

    let userpath = NSString.path(withComponents: [directory, filename])
    let fullpath = (userpath as NSString).standardizingPath

    if let content = try? String(contentsOfFile: fullpath) {
         return content
    } else { fatalError("file not found") }
}

If I try to use String in either case, it doesn't work, complaining type 'String' has no member 'path' etc.

I don't mind using NSString etc. I just want to understand the docs for Foundation types.

What platform are you on? My understanding is that this automatic conversion only happens on Apple platforms, not Windows or Linux.

I'm on macOS Ventura 13.3.1 (on an M1 MBA), in an Xcode playground.

In the cases where the String and NSString APIs don't align, the reason is generally that — in Swift-centric world — the omitted APIs aren't really regarded as properly belonging to the bridged Swift type's semantics.

In particular, Swift has no free-floating concept of a "path", since the concept is dependent on both platform and use-case.

Swift does embrace the concept of a path that's anchored in the URL world, via the URL and URLComponents types (which still come from Foundation).

That gives you 3 choices:

  1. Use a URL instead of a String to represent your NSString.

  2. Invent a custom MyPath type with syntax and semantics for your non-URL use-case.

  3. Stick with NSString APIs and bridge to/from String at the edges of your code.

2 Likes

Thank you, @QuinceyMorris. It makes more sense now.

I was looking at the URL and FileManager classes, but saw that URL was bridged to NSURL, so assumed I'd have the same issue. If I understand you correctly, I'd actually have more luck with those kinds of APIs, because they're carried over into Swift more directly.

It's been a few years, but I think path(withComponents:) did exist as an extension on String at some point, and its unavailability message pointed to a URL method instead: swift/NSStringAPI.swift at swift-5.0-RELEASE · apple/swift · GitHub

1 Like

@krilnon - I followed the comment in the code you linked to, and figured this should work (inside the function from above):

URL.fileURL(withPathComponents: [directory, filename])

It doesn't work though (URL doesn't have a fileURL member). However, it's fine if I change URL to NSURL, which is the issue I was expecting to have (I still need to directly use the bridged Foundation reference types in my code).

In case it helps, when I call methods on these Foundation types (NSString and NSURL), I get Swift types (String and URL) back (well, technically URL?).

It seems as though the Foundation types are "bridged" to return Swift-typed results, but the Swift types don't double as their corresponding Foundation types. That'd make some sense, but the docs were pretty clear that I didn't need to directly use Foundation types in Swift code (generally).

I don't see fileURL(withPathComponents:) available on URL. If you can tolerate using NSString / NSURL API - go for it, otherwise you'd need to implement your own version of the call that assembles file URL from components (perhaps by using appendPathComponent / appendingPathComponent in a loop.

@tera - Thanks for the advice.

I'm totally relaxed about using NSString and NSURL, and am happy to wrap them if they ever get too clunky. I was just worried that I was doing something wrong, based on the doc I linked to (I'm new to Swift, and macOS development).

Absurdly, NSURL.fileURL(withPathComponents:) returns a URL. Thanks, bridging magic! ¯\_(ツ)_/¯

Presumably, the intention at some point was not to expose this to Swift because it was seen as “not the right way”. (Otherwise it should have been bridged as URL.init(filePathComponents:) or similar.) I’m not sure what the “right way” would be in this case, though. My mind went to URLComponents, but it doesn’t expose an array of path components; URL does but it’s immutable. Two appendPathComponents or relative URLs would do it, yeah.

1 Like