Hi all! The Foundation team is working on bringing FormatStyle
and ParseStrategy
to URL
. We are introducing two types, URL.FormatStyle
and URL.ParseStrategy
, as a simple, familiar, and error-proof interface for you to configure URL format styles and parse strategies with sensible defaults. We are interested in everyone's feedback on these types. In particular, please let us know:
- What should
URL.FormatStyle
's default configuration be? Which components should we display and which ones should we hide? - Is
ComponentParseStrategy
(see details below) necessary? Can it be replaced by functions with parameters? For example, do you preferstrategy.port(.defaultValue(80))
orstrategy.port(required: false, defaultValue: 80)
?
Thanks, and let me know your thoughts!
URL FormatStyle
and ParseStrategy
- Proposal: FOU-NNNN
- Author(s): Charles Hu
- Status: Active review
Introduction
Foundation introduced a new pattern for formatting and parsing currency types such as Date
and Measurement
last year. This proposal aims to provide similar formatting and parsing capabilities to URL
via the introduction of URL.FormatStyle
and URL.ParseStrategy
. We hope to provide a clear, familiar, and error-proof interface for the developers to configure URL
format styles and parse strategies with sensible defaults, similar to the rest of the FormatStyle
and ParseStrategy
family.
Motivation
URL formatting is a frequently requested feature with many high-profile use cases such as the address bar in Safari and link previews in Messages. We want to provide a Foundation API that: 1) covers all essential features that these use cases need; and 2) provides a concise and familiar API surface by conforming to the existing FormatStyle
and ParseStrategy
protocols.
In addition to parsing URLs, URL.ParseStrategy
will also participate in regex pattern matching alongside the other Foundation
parse strategies, with the new String Processing library (which will eventually be part of the standard library). You will be able to use URL.ParseStrategy
as part of the regex builder to match URLs directly.
Proposed Solution and Example
We propose to introduce two types, URL.FormatStyle
and URL.ParseStrategy
that behave exactly like the other format styles and parse strategies we have today. In other words, you'll be able to format an URL
like this:
let url = URL("https://charles:password@www.example.com:8080/search?color=red#product")
// The default style:
// - Always displays the scheme, host and path
// - Never displays user, password, query, and fragment
// - Omits port if the scheme is HTTP family ("https" or "http")
let displayString = url.formatted() // https://www.example.com/search
// You can also configure the display strategy for each component:
let custom = url.formatted(
.url.scheme(.always)
.host(.omitSpecificSubdomains(["www"]))
.port(.always)) // https://example.com:8080/search
As a safety feature, URL.FormatStyle
will attempt to mitigate IDN homograph attacks. Simply put, URL.FormatStyle
will display the Punycode encoded hostname if the hostname contains "lookalike" Unicode characters:
// Not apple.com
print(URL("https://аррІе.com/").formatted()) // https://xn--80ak6aa4i.com
// Not google.com
print(URL("https://gooِgle.com/").formatted()) // https://xn--google-yri.com
URL.ParseStrategy
is also component based. You will be able to control which URL component is required to parse the URL
, as well as if a default value should be used if a component is missing:
// The default ParseStrategy requires `scheme` and `host` to exists in the URL, and
// it parses URLs leniently.
var strategy = URL.ParseStrategy()
try? strategy.parse("https://www.example.com/path") // returns an URL instance
try? strategy.parse("www.apple.com") // returns `nil` because scheme is missing
// Configure whether each component is required
strategy = URL.ParseStrategy(scheme: .required, host: .required, query: .required)
try? strategy.parse("https://www.cherry.com/path?name=peach") // returns a URL instance
try? strategy.parse("https://www.mango.com/path") // returns `nil` because query is missing
// Configure default values
strategy = URL.ParseStrategy(
scheme: .defaultValue("https"),
port: .defaultValue(8080))
// In this case:
// - The scheme is missing so `ParseStrategy` will use the default value "https"
// - The port is not missing, so `ParseStrategy` will keep the original value
try? strategy.parse("www.orange.com:1234") // returns an URL instance: https://www.orange.com:1234
// In this case both fields are missing so `ParseStrategy` will use the default values for both
try? strategy.parse("www.strawberry.com") // returns an URL instance: https://www.strawberry.com:8080
// Error handling
do {
strategy = URL.ParseStrategy()
let _ = try strategy.parse("www.blueberry.com")
} catch {
print(error)
// Error Domain=NSCocoaErrorDomain Code=2048 "Cannot parse
// www.blueberry.com. String should adhere to the preferred format
// of the locale, such as https://www.example.com/path." UserInfo=
// {NSDebugDescription=Cannot parse www.apple.com. String should
// adhere to the preferred format of the locale, such as
// https://www.example.com/path.}
}
You can also use URL.ParseStrategy
within a Regex builder (from the new String Pattern Matching library) directly to match URLs:
let text = "https://www.example.com/path 2022-03-014T16:20:32Z"
let regex = Regex {
capture(.url)
capture(.iso8601Date)
}
let match = text.match(regex)
// Both the URL and the Date should be automatically captured and parsed.
print(match.match) // ("https://www.example.com/path 2022-03-014T16:20:32Z", URL, Date)
We are also extending the URLComponents
' parser to support Internationalized Domain Names (IDNs) as part of this effort. You will now be able to parse, match, and construct URLs with non-ASCII host names like these:
let strategy = URL.ParseStrategy()
// Yes, these are real URLs in use :P
try? strategy.parse("https://👁👄👁.fm") // returns an URL instance
let regex = Regex {
capture(.url)
}
print("https://🐮.ws".match(regex).match) // ("https://🐮.ws", URL)
let components = URLComponents(string: "http://見.香港/")
print(components.url) // returns an URL instance
Detailed Design
Formatting URLs
We propose to introduce URL.FormatStyle
to describe options for formatting an URL
instance into user-visible strings:
extension URL {
/// Strategies for formatting an `URL`.
@available(TBD)
public struct FormatStyle : Codable, Hashable, Sendable {
/// The strategy to display the `scheme` component.
var scheme: ComponentDisplayStrategy
/// The strategy to display the `user` component.
var user: ComponentDisplayStrategy
/// The strategy to display the `password` component.
var password: ComponentDisplayStrategy
/// The strategy to display the `host` component.
var host: HostDisplayStrategy
/// The strategy to display the `port` component.
var port: ComponentDisplayStrategy
/// The strategy to display the `path` component.
var path: ComponentDisplayStrategy
/// The strategy to display the `query` component.
var query: ComponentDisplayStrategy
/// The strategy to display the `fragment` component.
var fragment: ComponentDisplayStrategy
/// Creates a new `FormatStyle` with the given configurations.
/// - Parameters:
/// - scheme: The strategy to use for formatting the `scheme`.
/// - user: The strategy to use for formatting the `user`.
/// - password: The strategy to use for formatting the `password`.
/// - host: The strategy to use for formatting the `host`.
/// - port: The strategy to use for formatting the `port`.
/// - path: The strategy to use for formatting the `path`.
/// - query: The strategy to use for formatting the `query`.
/// - fragment: The strategy to use for formatting the `fragment`.
public init(
scheme: ComponentDisplayStrategy = .always,
user: ComponentDisplayStrategy = .never,
password: ComponentDisplayStrategy = .never,
host: HostDisplayStrategy = .always,
port: ComponentDisplayStrategy = .omitIfHTTPFamily,
path: ComponentDisplayStrategy = .always,
query: ComponentDisplayStrategy = .never,
fragment: ComponentDisplayStrategy = .never)
}
}
You can use a modifier syntax to customize an URL.FormatStyle
:
let style = URL.FormatStyle()
.scheme(.omitIfHTTPFamily)
.user(.never)
.password(.never)
.host(.omitSpecificSubdomains(["www", "mobile", "m"]))
.port(.omitIfHTTPFamily)
.path(.always)
.query(.never)
.fragment(.never)
let url = URL("https://charles:pa$$word@www.pear.com:1234/search?color=blue#price")
print(url.formatted(style)) // pear.com/search
These URL component modifiers are defined as follows:
@available(TBD)
extension URL.FormatStyle {
public func scheme(_ strategy: ComponentDisplayStrategy = .always) -> Self
public func user(_ strategy: ComponentDisplayStrategy = .never) -> Self
public func password(_ strategy: ComponentDisplayStrategy = .never) -> Self
public func host(_ strategy: HostDisplayStrategy = .always) -> Self
public func port(_ strategy: ComponentDisplayStrategy = .omitIfHTTPFamily) -> Self
public func path(_ strategy: ComponentDisplayStrategy = .always) -> Self
public func query(_ strategy: ComponentDisplayStrategy = .never) -> Self
public func fragment(_ strategy: ComponentDisplayStrategy = .never) -> Self
}
URL.FormatStyle.Component
We want to support the notion of "conditionally apply" the display strategy to each component. For example, you may choose to hide the scheme
if it's in the HTTP family (http
or https
); or you may choose to display the port
if it's not one of the known ports. In order to achieve this goal, we need to first define the list of components:
extension URL.FormatStyle {
@available(TBD)
public enum Component : Int, Codable, Hashable, Sendable, CustomStringConvertible {
case scheme
case user
case password
case host
case port
case path
case query
case fragment
public var description: String
}
}
ComponentDisplayStrategy
and HostDisplayStrategy
will use URL.FormatStyle.Component
to create conditional strategies.
ComponentDisplayStrategy
and HostDisplayStrategy
The display strategies that URL.FormatStyle
directly uses comes in two versions:
-
HostDisplayStrategy
is a specialized version for thehost
component. It comes with additional formatting features specifically forhost
s. -
ComponentDisplayStrategy
is the generic version used by all other components. It simply represents whether a component should be displayed or omitted given a condition.
extension URL.FormatStyle {
/// Specifies the display strategy for a component, including whether to display or omit the
/// component and the condition to do so.
@available(TBD)
public struct ComponentDisplayStrategy : Codable, Hashable, CustomStringConvertible, Sendable {
public var description: String
/// Creates a display strategy to always display the component.
public static var always: Self
/// Creates a display strategy to always omit the component.
public static var never: Self
/// Creates a display strategy to display the component when the component meets the requirements
public static func displayWhen(_ component: URL.FormatStyle.Component, matches requirements: Set<String>) -> Self
/// Creates a display strategy to omit the component when the component meets the requirements
public static func omitWhen(_ component: URL.FormatStyle.Component, matches requirements: Set<String>) -> Self
/// Creates a display strategy to omit the component when the URL's scheme
/// is `http` or `https`.
public static var omitIfHTTPFamily: Self
}
}
Here are some examples:
let style: URL.FormatStyle = .init()
// Omits the scheme if it's `http` or `https`
.scheme(.omitIfHTTPFamily)
// Only displays the user if it's "Charles"
.user(.displayWhen(.user, matches: ["Charles"]))
// Never omit the password
.password(.never)
// Always display the path
.path(.always)
// Omit the port if it's either 8080 or 20
.port(.omitWhen(.port, matches: ["8080", "20"]))
URL("https://tim:pa$$w0rd@www.lychee.com:42/about").formatted(style)
// www.lychee.com:42/about
URL("ftp://charles:pa$$w0rd@www.coconut.com:8080/files").formatted(style)
// ftp://charles@www.coconut.com/files
HostDisplayStrategy
adds two additional options to manipulate the subdomains of a host:
-
omitMultiLevelSubdomains
omits all additional subdomains if there are more than two subdomains in addition to the top-level-domain (TLD). For example:api.code.developer.apple.com
is displayed asdeveloper.apple.com
(TLD: "com"), whereasapi.code.developer.apple.com.cn
is displayed asdeveloper.apple.com.cn
(TLD: "com.cn") -
omitSpecificSubdomains
omits the first (the leftmost) subdomain if it's in the given set. For example: if the subdomain to omit is "mobile", thenmobile.apple.com
will be displayed asapple.com
whiledeveloper.mobile.apple.com
will not be changed.
You can also combine these two options to further clean up the host (see examples below).
extension URL.FormatStyle {
/// Specifies the display strategy for displaying the host component
@available(TBD)
public struct HostDisplayStrategy : Codable, Hashable, CustomStringConvertible, Sendable {
public var description: String
/// Creates a display strategy to always display the host.
public static var always: Self
/// Creates a display strategy to always omit the host.
public static var never: Self
/// Creates a display strategy to display the host if the component matches the requirements
public static func displayWhen(_ component: URL.FormatStyle.Component, matches requirements: Set<String>) -> Self
/// Creates a display strategy to omit the host if the component matches the requirements
public static func omitWhen(_ component: URL.FormatStyle.Component, matches requirements: Set<String>) -> Self
/// Creates a display strategy to omit the host if the URL's scheme
/// is `http` or `https`.
public static var omitIfHTTPFamily: Self
/// Creates a display strategy to manipulate the subdomains of a host
/// - Parameters:
/// - subdomainsToOmit: specifies a set of subdomains to omit
/// - omitMultiLevelSubdomains: if `true`, additional subdomains (subdomains more than 2 + TLDs)
/// will be omitted.
public static func omitSpecificSubdomains(
_ subdomainsToOmit: Set<String> = Set(),
includeMultiLevelSubdomains omitMultiLevelSubdomains: Bool = false) -> Self
/// Creates a display strategy to manipulate the subdomains of a host if
/// the given component matches the requirements
/// - Parameters:
/// - subdomainsToOmit: specifies a set of subdomains to omit
/// - omitMultiLevelSubdomains: if `true`, additional subdomains (subdomains more than 2 + TLDs)
// will be omitted.
/// - component: the component to check requirements for
/// - requirements: the requirements to check
public static func omitSpecificSubdomains(
_ subdomainsToOmit: Set<String> = Set(),
includeMultiLevelSubdomains omitMultiLevelSubdomains: Bool = false,
when component: URL.FormatStyle.Component,
matches requirements: Set<String>) -> Self
}
}
Here are some examples of HostDisplayStrategy
that changes how subdomains are displayed:
var style: URL.FormatStyle = .init()
.scheme(.never)
// Omit the "www" subdomain if it's the first subdomain
.host(.omitSpecificSubdomains(["www"]))
URL("https://www.banana.com/about").formatted(style)
// banana.com/about
URL("https://developer.www.banana.com/about").formatted(style)
// developer.www.banana.com/about (not changed because www isn't the first subdomain)
style = style
// Only omit multi-level subdomains
.host(.omitSpecificSubdomains([], includeMultiLevelSubdomains: true))
URL("https://api.docs.developers.grapefruit.com/about").formatted(style)
// developers.grapefruit.com/about
style = style
// Omit "www" and "mobile" subdomains as well as multi-level subdomains
.host(.omitSpecificSubdomains(["mobile", "www"], includeMultiLevelSubdomains: true))
URL("https://api.docs.mobile.pineapple.com/metal").formatted(style)
// pineapple.com/metal
URL("https://mobile.www.m.pineapple.com/metal").formatted(style)
// m.pineapple.com/metal
style = style
// Omit the "mobile" subdomain and multi-level subdomains IF the URL is in the HTTP family
.host(.omitSpecificSubdomains(
["mobile"],
includeMultiLevelSubdomains: true,
when: .scheme, matches: ["http", "https"]))
URL("https://docs.mobile.lemon.com/page").formatted(style)
// lemon.com/page
URL("ftp://docs.mobile.lemon.com/data").formatted(style)
// docs.mobile.lemon.com/data (subdomains are not modified because the condition is not met)
Note: omitting multi-level subdomains will not be supported on Linux.
The default style to URL.FormatStyle
(i.e. when you create a "blank" FormatStyle
without any modifications) always displays the host
and path
, omits the port
and scheme
if the URL is in the HTTP family, and never displays the rest of the components. We believe this is a sensible default for most use cases.
Finally, to align URL.FormatStyle
's API surface with other Foundation vended FormatStyle
s, we propose to introduce these miscellaneous changes:
- Introduce two
formatted
methods onURL
that formats the instance with a given style; - Extend
URL.FormatStyle
to conform toParsableFormatStyle
so you will be able to construct anURL.ParseStrategy
from a format style; - Introduce a static variable
url
onFormatStyle
andParseableFormatStyle
constrainted toSelf
as shortcuts to initializing the default format style.
extension URL {
/// Converts `self` to its textual representation.
/// - Parameter format: The format for formatting `self`.
/// - Returns: A representation of `self` using the given `format`. The type of
/// the representation is specified by `FormatStyle.FormatOutput`
@available(TBD)
public func formatted<F: Foundation.FormatStyle>(_ format: F) -> F.FormatOutput where F.FormatInput == URL
public func formatted() -> String
}
@available(TBD)
extension URL.FormatStyle : ParseableFormatStyle {
public var parseStrategy: URL.ParseStrategy { get }
}
@available(TBD)
public extension FormatStyle where Self == URL.FormatStyle {
static var url: Self { get }
}
@available(TBD)
public extension ParseableFormatStyle where Self == URL.FormatStyle {
static var url: Self { get }
}
Parsing URLs
We propose to introduce URL.PraseStrategy
to describe options for parsing an URL string into an URL
instance:
extension URL {
/// Options for parsing string representations of URLs to create an `URL` instance.
@available(TBD)
public struct ParseStrategy : Codable, Hashable, Sendable {
/// The strategy to parse the `scheme` component.
var scheme: ComponentParseStrategy<String>
/// The strategy to parse the `user` component.
var user: ComponentParseStrategy<String>
/// The strategy to parse the `password` component.
var password: ComponentParseStrategy<String>
/// The strategy to parse the `host` component.
var host: ComponentParseStrategy<String>
/// The strategy to parse the `port` component.
var port: ComponentParseStrategy<Int>
/// The strategy to parse the `path` component.
var path: ComponentParseStrategy<String>
/// The strategy to parse the `query` component.
var query: ComponentParseStrategy<String>
/// The strategy to parse the `fragment` component.
var fragment: ComponentParseStrategy<String>
/// Creates a new `ParseStrategy` with the given configurations.
/// - Parameters:
/// - scheme: The strategy to use for parsing the `scheme`.
/// - user: The strategy to use for parsing the `user`.
/// - password: The strategy to use for parsing the `password`.
/// - host: The strategy to use for parsing the `host`.
/// - port: The strategy to use for parsing the `port`.
/// - path: The strategy to use for parsing the `path`.
/// - query: The strategy to use for parsing the `query`.
/// - fragment: The strategy to use for parsing the `fragment`.
public init(
scheme: ComponentParseStrategy<String> = .required,
user: ComponentParseStrategy<String> = .optional,
password: ComponentParseStrategy<String> = .optional,
host: ComponentParseStrategy<String> = .required,
port: ComponentParseStrategy<Int> = .optional,
path: ComponentParseStrategy<String> = .optional,
query: ComponentParseStrategy<String> = .optional,
fragment: ComponentParseStrategy<String> = .optional)
}
}
@available(TBD)
extension ParseStrategy where Self == URL.ParseStrategy {
public static var url: Self
}
@available(TBD)
extension URL {
/// Creates a new `URL` by parsing the given representation.
/// - Parameters:
/// - value: A representation of an URL. The type of the representation is specified
/// by `ParseStrategy.ParseInput`.
/// - strategy: The parse strategy to parse `value` whose `ParseInput` is `URL`.
public init<T: Foundation.ParseStrategy>(_ value: T.ParseInput, strategy: T) throws where T.ParseOutput == Self
}
You can use a modifier syntax to customize an URL.ParseStrategy
(similar to URL.FormatStyle
):
let strategy = URL.ParseStrategy()
.scheme(.defaultValue("https"))
.user(.optional)
.password(.optional)
.host(.required)
.port(.defaultValue(8080))
.path(.optional)
.query(.optional)
.fragment(.optional)
let text = "www.watermelon.com/about"
let url = try? strategy.parse(text) // https://www.watermelon.com:8080/about
These modifiers are defined as follows:
@available(TBD)
extension URL.ParseStrategy {
public func scheme(_ strategy: ComponentParseStrategy<String> = .required) -> Self
public func user(_ strategy: ComponentParseStrategy<String> = .optional) -> Self
public func password(_ strategy: ComponentParseStrategy<String> = .optional) -> Self
public func host(_ strategy: ComponentParseStrategy<String> = .required) -> Self
public func port(_ strategy: ComponentParseStrategy<Int> = .optional) -> Self
public func path(_ strategy: ComponentParseStrategy<String> = .optional) -> Self
public func query(_ strategy: ComponentParseStrategy<String> = .optional) -> Self
public func fragment(_ strategy: ComponentParseStrategy<String> = .optional) -> Self
}
ComponentParseStrategy
URL.ParseStrategy
uses ComponentParseStrategy
(formally URL.ParseStrategy.ComponentParseStrategy
) to determine the rules to parse each URL component:
extension URL.ParseStrategy {
/// Specifies the strategy to use to parse each component.
@available(TBD)
public enum ComponentParseStrategy<Component : Codable & Hashable & Sendable> : Codable, Hashable, CustomStringConvertible, Sendable {
/// Denotes that the component is required to exists in order to consider the URL valid
case required
/// Denotes that the component is optional
case optional
/// If the component is missing, assume it has the attached default value
case defaultValue(Component)
public var description: String
}
}
In addition to the standard .required
and .optional
case, ComponentParseStrategy
also provides a third case .defaultValue(Component)
that allows the developers to specify a default value for each component. This option is especially useful when the data being parsed is known to miss certain fields (most commonly the scheme
). Here are some examples:
let strategy: URL.Strategy = .init()
// When the URL does not have scheme, use "http" as the scheme
.scheme(.defaultValue("http"))
// When the URL does not have the port value, use the default `80` port
.port(.defaultValue(80))
// The returned URL will already have the default values filled in
try? strategy.parse("www.lychee.com") // http://www.lychee.com:80
// `ParseStrategy` will only fill in the missing values. In this case
// it will only fill in the scheme
try? strategy.parse("www.gooseberry.com:8090") // http://www.gooseberry.com:8090
URL.ParseStrategy
in String Processing
URL.ParseStrategy
will also participate in Regex powered String Processing as one of the CustomMatchingRegexComponent
:
@available(TBD)
extension URL.ParseStrategy : CustomMatchingRegexComponent {
typealias Match = URL
}
We will also extend the RegexProtocol
with two static members as the dot syntax (.url
) shortcuts to URL.ParseStrategy
:
extension RegexProtocol where Self == URL.ParseStrategy {
/// Creates a parse strategy with default configurations
public static var url: Self
/// Creates a custom parse strategy with given required components
public static func url(
scheme: ComponentParseStrategy<String> = .required,
user: ComponentParseStrategy<String> = .optional,
password: ComponentParseStrategy<String> = .optional,
host: ComponentParseStrategy<String> = .required,
port: ComponentParseStrategy<Int> = .optional,
path: ComponentParseStrategy<String> = .optional,
query: ComponentParseStrategy<String> = .optional,
fragment: ComponentParseStrategy<String> = .optional) -> Self
}
Please refer back to the Motivation section for some example usages of URL parsing and string matching.
Note: the default configuration for URL.ParseStrategy
requires the scheme and host to exist to consider a string a valid URL. It's important to have some requirements when parsing URLs because the URL standard is pretty broad. Many seemly "not an URL" strings, such as Foundation.framework
, or simply just Foundation
, can be considered as valid URLs. As a result, component requirements are essential when using URL.ParseStrategy
to perform pattern matching -- a "no requirement" strategy will simply match any string up to the next whitespace.
Extending URLComponents
to Support Internationalized Domain Names
We posted a proposal on the Swift forums and you told us you'd like to update URL
and URLComponents
' parser to support Internationalized Domain Names (IDNs) such as http://見.香港/
, or http://இலங்கை.icom.museum
. We want to take this opportunity to update URLComponents
' parser (since it's more modern than URL
's parser) to support these domain names with automatic Punycode encoding. As a result, URLComponents.percentEncodedHost
no longer makes sense because IDNs must be Punycode encoded instead of percent-encoded. We propose to introduce a new property URLComponents.encodedHost
to allow get
and set
of Punycode encoded host and soft-deprecate URLComponents.percentEncodedHost
:
public struct URLComponents {
...
@available(macOS, introduced: 10.9, deprecated: 100000.0, message: "Use encodedHost instead")
@available(iOS, introduced: 7.0, deprecated: 100000.0, message: "Use encodedHost instead")
@available(tvOS, introduced: 9.0, deprecated: 100000.0, message: "Use encodedHost instead")
@available(watchOS, introduced: 2.0, deprecated: 100000.0, message: "Use encodedHost instead")
public var percentEncodedHost: String?
@available(TBD)
public var encodedHost: String?
}
Here are some examples of Punycode encoded hosts:
var urlComponents = URLComponents(string: "http://見.香港")!
print(urlComponents.host) // 見.香港
print(urlComponents.encodedHost) // xn--nw2a.xn--j6w193g
// Setting raw host
urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "👁👄👁.fm"
print(urlComponents.encodedHost) // xn--mp8hai.fm
print(urlComponents.string) // https://xn--mp8hai.fm
// Setting encoded host
urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.encodedHost = "xn--2o8h.ws"
print(urlComponents.host) // 🐮.ws
print(urlComponents.string) // https://xn--2o8h.ws
Note: we decided to not update URL
's parser for backward compatibility. This means you can only construct an URL with IDNs via URLComponents
. Constructing internationalized URLs via URL
's constructor directly (URL("https://👁👄👁.fm")
) will still return nil
.
Impact on Existing Code
Minimal. This proposal mostly contains additive API Surface changes except URLComponents
' IDN adoption.
Alternatives Considered
None.