Hi all, Apple’s Foundation team is working on some improvements to URL
, focused on simple changes we can make to improve the ergonomics of using it. Given that this is such a widely used type in Swift, we’re interested in everyone’s input on these ideas. In particular, please let us know if (1) you think this will improve the ergonomics of using URL or (2) if there are other straightforward changes we should consider to help. Thanks!
URL Enhancements
- Proposal: FOU-NNNN
- Author(s): Charles Hu
- Status: Active review
Introduction
URL
is one of the most common types used by Foundation for File IO as well as network-related tasks. The goal of this proposal is to improve the ergonomics of URL
by introducing some convenience methods and renaming some verbose/confusing ones. Specifically, we proposed to:
- Introduce a new
StaticString
initializer - Refine the existing "file path" initializers
- Refine
appendingPathComponent
and friends - Introduce common directories as static
URL
properties
We will discuss each improvement in the following sections.
The New StaticString
Initializer
URL
represents two concepts: 1) internet address such as https://www.apple.com
initialize by init?(string: String)
, and 2) file path such as /System/Library/
initialized by init(fileURLWithPath: String)
. We propose to introduce a new StaticString
initializer that could initialize URL
for both concepts. This initializer will require the StaticString
to contain the URL scheme and initialize String
s with the file:///
scheme as file paths and everything else as web addresses. Because this initializer takes a StaticString
known at compile-time, it considers "schemeless" String
s as a programmer error and assert
s the existence of a scheme. It will also assert
any invalid web addresses, which mainly means empty strings and strings with invalid characters. Here are some examples:
String Literal | URL Type | URL absoluteString
|
---|---|---|
"https://www.apple.com" |
Web Address | https://www.apple.com |
"file:///var/mobile" |
File Path | file:///var/mobile |
"www.apple.com" |
- | Assertion failure |
/System/Library/Frameworks" |
- | Assertion failure |
"file://www.ap le.com" |
File Path | file:///current/dir/www.ap%20le.com |
"https://www.ap le.com" |
Web Address | Assertion failure |
@available(TBD)
extension URL {
/// Initialize an URL with a String literal. This initializer **requires** the
/// string literal contains a URL scheme such as `file://` or `https://` to correctly
/// interpret the URL. It will `assert` the validate of web addresses
/// (i.e. URLs that doesn't start with the `file` scheme).
init(_ string: StaticString)
}
Revisit the FilePath
Initializers
The new StaticString
initializer conflicts with the existing FilePath
based initializer because neither has an argument label and FilePath
conforms to ExpressibleByStringLiteral
. The FilePath
initializer is problematic because it could lead to unexpected results. For example, contrary to the assumption that most developer will have, URL("https://www.apple.com")
actually initializes a file path (isFileURL
returns true
) because "https://www.apple.com"
is interpreted as a FilePath
due to ExpressibleByStringLiteral
conformance. We therefore propose to rename the FilePath
family initializers to init(filePath: FilePath)
and deprecate and @_disfavoredOverload
the original versions.
Similarly, we propose to rename the existing family of file path initializers, init(fileURLWithPath path: String)
to init(filePath: String)
, to slightly improve the ergonomics of these methods due to the more concise naming. This will, of course, conflict with the new FilePath
initializer, so we will mark them as @_disfavoredOverload
because they require additional transformations to and from FilePath
.
extension URL {
@available(*, deprecated, renamed: "init?(filePath:)")
@_disfavoredOverload
public init?(_ path: FilePath)
@available(*, deprecated, renamed: "init?(filePath:isDirectory)")
@_disfavoredOverload
public init?(_ path: FilePath, isDirectory: Bool)
@available(TBD)
@_disfavoredOverload
public init?(filePath: FilePath)
@available(TBD)
@_disfavoredOverload
public init?(filePath: FilePath, isDirectory: Bool)
}
extension URL {
@available(TBD)
public init(filePath: String)
@available(TBD)
public init(filePath: String, isDirectory: Bool)
@available(TBD)
public init(filePath: String, relativeTo: URL?)
@available(TBD)
public init(filePath: String, isDirectory: Bool, relativeTo: URL?)
@available(*, deprecated, renamed: "init(filePath:)")
public init(fileURLWithPath: String)
@available(*, deprecated, renamed: "init(filePath:isDirectory)")
public init(fileURLWithPath: String, isDirectory: Bool)
@available(*, deprecated, renamed: "init(filePath:relativeTo)")
public init(fileURLWithPath: String, relativeTo: URL?)
@available(*, deprecated, renamed: "init(filePath:isDirectory:relativeTo)")
public init(fileURLWithPath: String, isDirectory: Bool, relativeTo: URL?)
}
Revisit appendingPathComponent
and Friends
We also propose to rename the (painfully) long, but popular appendingPathComponent()
and friends to .appending(path:)
, as well as adding a new .appending(paths:)
variant that takes a collection of path components:
extension URL {
@available(TBD)
public mutating func append(path: String)
@available(TBD)
public mutating func append(path: String, isDirectory: Bool)
@available(TBD)
public func appending(path: String) -> URL
@available(TBD)
public func appending(path: String, isDirectory: Bool) -> URL
@available(TBD)
public mutating func append<C: Collection>(paths: C, isDirectory: Bool? = nil) where C.Element == String
@available(TBD)
public func appending<C: Collection>(paths: C, isDirectory: Bool? = nil) where C.Element == String -> URL
@available(*, deprecated, renamed: "append(path:)")
public mutating func appendPathComponent(String)
@available(*, deprecated, renamed: "append(path:isDirectory)")
public mutating func appendPathComponent(String, isDirectory: Bool)
@available(*, deprecated, renamed: "appending(path:)")
public func appendingPathComponent(String) -> URL
@available(*, deprecated, renamed: "appending(path:isDirectory)")
public func appendingPathComponent(String, isDirectory: Bool) -> URL
}
The addition of .appending(paths:)
will make appending multiple path components to URL
simpler. For example:
let baseURL: URL = ...
let id = UUID().uuidString
// Before
let photoURL = baseURL
.appendingPathComponent("Photos")
.appendingPathComponent("\(id.first!)")
.appendingPathComponent(id)
// After
let photoURL = baseURL.appending(paths: ["Photos", "\(id.first!)", id])
Common Directories as Static URL Properties
We propose to add all "get URL" style methods from FileManager
to URL
as static methods. This allows the call site to use Swift's powerful static member lookup to get the URLs to predefined directories instead of always needing to spell out FileManager.default
. We also propose to add a few more static directory URLs that correspond to FileManager.SearchPathDirectory
. Note that these are not stored properties and will not run in O(1). We will add documentations to specify that.
@available(TBD)
extension URL {
/// The working directory of the current process.
/// Calling this property will issue a `getcwd` syscall.
public static var currentDirectory: URL
/// The home directory for the current user (~/).
/// Complexity: O(1)
public static var homeDirectory: URL
/// The temporary directory for the current user.
/// Complexity: O(1)
public static var temporaryDirectory: URL
/// Discardable cache files directory for the
/// current user. (~/Library/Caches).
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var cachesDirectory: URL
/// Supported applications (/Applications).
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var applicationDirectory: URL
/// Various user-visible documentation, support, and configuration
/// files for the current user (~/Library).
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var libraryDirectory: URL
/// User home directories (/Users).
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var userDirectory: URL
/// Documents directory for the current user (~/Documents)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var documentsDirectory: URL
/// Desktop directory for the current user (~/Desktop)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var desktopDirectory: URL
/// Application support files for the current
/// user (~/Library/Application Support)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var applicationSupportDirectory: URL
/// Downloads directory for the current user (~/Downloads)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var downloadsDirectory: URL
/// Movies directory for the current user (~/Movies)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var moviesDirectory: URL
/// Music directory for the current user (~/Music)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var musicDirectory: URL
/// Pictures directory for the current user (~/Pictures)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var picturesDirectory: URL
/// The user’s Public sharing directory (~/Public)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var sharedPublicDirectory: URL
/// Trash directory for the current user (~/.Trash)
/// Complexity: O(n) where n is the number of significant directories
/// specified by `FileManager.SearchPathDirectory`
public static var trashDirectory: URL
/// Returns the home directory for the specified user.
public static func homeDirectory(forUser user: String) -> URL?
/// Locates and optionally creates the specified common directory in a domain.
///
/// - parameter directory: The search path directory. The supported values are
/// described in FileManager.SearchPathDirectory.
/// - parameter domain: The file system domain to search. The value for this
/// parameter is one of the constants described in
/// `FileManager.SearchPathDomainMask`. You should specify only one domain for
/// your search and you may not specify the allDomainsMask constant for
/// this parameter.
/// - parameter url: The file URL used to determine the location of the returned
/// URL. Only the volume of this parameter is used. This parameter is ignored
/// unless the directory parameter contains the value
/// `FileManager.SearchPathDirectory.itemReplacementDirectory` and the domain
/// parameter contains the value `userDomainMask`.
/// - parameter shouldCreate: Whether to create the directory if it does not
/// already exist.
public init(
for directory: FileManager.SearchPathDirectory,
in domain: FileManager.SearchPathDomainMask,
appropriateFor url: URL?,
create shouldCreate: Bool) throws
}
Now code for common file tasks such as writing files to the Downloads directory will be much cleaner:
let secretData = ...
// Before
let downloadDirectoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
let targetFolderURL = downloadDirectoryURL.appendingPathComponent("TopSecrets")
try FileManager.default.createDirectory(at: targetFolderURL, withIntermediateDirectories: true, attributes: nil)
let secretDataURL = targetFolderURL.appendingPathComponent("Secret.file")
try secretData.write(to: secretDataURL)
// After
let targetFolderURL: URL = .downloadsDirectory.appending(path: "TopSecrets")
try FileManager.default.createDirectory(at: targetFolderURL, withIntermediateDirectories: true, attributes: nil)
try secretData.write(to: targetFolderURL.appending(path: "Secret.file"))
Impact on Existing Code
Very minimal. Most changes introduced are additive with a few additional method renames.
Alternatives Considered
Use FilePath
for File IO Tasks
Swift System
introduced FilePath
to represent a location on the file system, which functionally overlaps with URL
for File IO tasks. Although FilePath
is more lightweight and has better cross-platform support, replacing URL
with FilePath
for File IO tasks in Foundation could lead to two major issues:
- Bigger impact on existing code. Callers of
FileManager
will need to be updated to useFilePath
. - URL supports resource value caching, a feature that many systems depends on.
FilePath
will need to support fetching and cachingURLResourceValues
before it could replaceURL
for file tasks.