String.addingPercentEncoding(withAllowedCharacters:) が何をエンコードするか気になったので調べてみました。
ついでに URLComponents の queryItems の value 部分と、JavaScriptCore の encodeURI および encodeURIComponent とも比較してみました。悩ましい。
ちなみに、記号は全部エンコードしちゃおうと思って .alphanumerics を使うと、アクセント符号付きのアルファベット (é など) やいわゆる全角英数字がエンコードされないという罠があります。
|CharacterSet| |!|"|#|$|%|&|'|(|)|*|+|,|-|.|/|:|;|<|=|>|?|@|[||]|^|_|`|{|||}|~|
|:--|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|urlFragmentAllowed|✅||✅|✅||✅|||||||||||||✅||✅|||✅|✅|✅|✅||✅|✅|✅|✅||
|urlHostAllowed|✅||✅|✅||✅||||||||||✅|||✅||✅|✅|✅||✅||✅||✅|✅|✅|✅||
|urlPasswordAllowed|✅||✅|✅||✅||||||||||✅|✅||✅||✅|✅|✅|✅|✅|✅|✅||✅|✅|✅|✅||
|urlPathAllowed|✅||✅|✅||✅||||||||||||✅|✅||✅|✅||✅|✅|✅|✅||✅|✅|✅|✅||
|urlQueryAllowed|✅||✅|✅||✅|||||||||||||✅||✅|||✅|✅|✅|✅||✅|✅|✅|✅||
|urlUserAllowed|✅||✅|✅||✅||||||||||✅|✅||✅||✅|✅|✅|✅|✅|✅|✅||✅|✅|✅|✅||
|URLComponents|✅||✅|✅||✅|✅||||||||||||✅|✅|✅|||✅|✅|✅|✅||✅|✅|✅|✅||
|encodeURI|✅||✅|||✅|||||||||||||✅||✅|||✅|✅|✅|✅||✅|✅|✅|✅||
|encodeURIComponent|✅||✅|✅|✅|✅|✅|||||✅|✅|||✅|✅|✅|✅|✅|✅|✅|✅|✅|✅|✅|✅||✅|✅|✅|✅||
import Foundation
import JavaScriptCore
let asciiSymbols = (0x20...0x7e)
    .map { Unicode.Scalar($0) }
    .filter { !CharacterSet.alphanumerics.contains($0) }
let allowdChars = { (str: String) -> String in
    let regexp = try! NSRegularExpression(pattern: "%[0-9A-F][0-9A-F]")
    let range = NSRange(str.startIndex..., in: str)
    return regexp.stringByReplacingMatches(in: str, range: range, withTemplate: "")
}
let urlComponents = { () -> String in
    var comp = URLComponents(string: "https://example.com/")!
    comp.queryItems = [URLQueryItem(name: "q", value: asciiSymbols.map { String($0) }.joined())]
    return allowdChars(comp.url!.absoluteString.components(separatedBy: "=")[1])
}()
let context = JSContext()!
context.evaluateScript("var ascii = ''; for (var i = 0x20; i < 0x7f; i++) ascii += String.fromCharCode(i)")
context.evaluateScript("var result = encodeURI(ascii)")
let encodeURI = allowdChars(context.objectForKeyedSubscript("result")!.toString()!)
context.evaluateScript("var result = encodeURIComponent(ascii)")
let encodeURIComponent = allowdChars(context.objectForKeyedSubscript("result")!.toString()!)
let charsets: [(String, CharacterSet)] = [
    ("urlFragmentAllowed", .urlFragmentAllowed),
    ("urlHostAllowed",     .urlHostAllowed),
    ("urlPasswordAllowed", .urlPasswordAllowed),
    ("urlPathAllowed",     .urlPathAllowed),
    ("urlQueryAllowed",    .urlQueryAllowed),
    ("urlUserAllowed",     .urlUserAllowed),
    ("URLComponents",      CharacterSet(charactersIn: urlComponents)),
    ("encodeURI",          CharacterSet(charactersIn: encodeURI)),
    ("encodeURIComponent", CharacterSet(charactersIn: encodeURIComponent)),
]
print("|CharacterSet|" + asciiSymbols.map { String($0) }.map { ($0 == "|" ? "|" : $0) + "|" }.joined())
print("|:--|" + asciiSymbols.map { _ in ":-:|" }.joined())
for cs in charsets {
    print("|\(cs.0)|" + asciiSymbols.map { (cs.1.contains($0) ? "" : "✅") + "|" }.joined())
}