Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Uses the NaturalLanguage framework combined with a thesaurus API to replace adjectives in a sentence with synonyms. This is designed to be run in a Swift Playground.
import UIKit
import NaturalLanguage
import PlaygroundSupport
//: Your secret API key from `` goes here.
let thesaurusKey = ""
//: The string you want to work on.
var testString = "The bright sun set behind the green hills. Thin clouds streaked the red sky."
//: Since we are making an API request as part of this playground, we need to allow it time to finish.
PlaygroundPage.current.needsIndefiniteExecution = true
/// The API returns an array of this data type. We're going to ignore everything except the synonyms.
struct ThesaurusResponse: Decodable {
struct Metadata: Decodable {
/// An array of synonym arrays. Each array is related to a different homograph of the query. Picking the right
/// one by context will require a deeper analysis of the API response.
let synonyms: [[String]]
enum CodingKeys: String, CodingKey {
case synonyms = "syns"
/// The top-level wrapper for metadata.
let meta: Metadata
/// Keep track of where in the string the word is we're going to replace so that we can do efficient string replacement.
/// Also, this will prevent all homographs identical to the query from being replaced simultaneously.
struct ReplacementTarget {
let value: String
let range: Range<String.Index>
/// Create a URLRequest for the dictionary API.
/// - Parameter query: The string which we are requesting synonyms for.
/// - Returns: A URLRequest that can be executed to query the API.
func createRequest(for query: String) -> URLRequest {
guard let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
fatalError("The query '\(query)' could not be requested.")
var urlComps = URLComponents(string: "\(escapedQuery)")!
urlComps.queryItems = [URLQueryItem(name: "key", value: thesaurusKey)]
var urlRequest = URLRequest(url: urlComps.url!)
urlRequest.httpMethod = "GET"
return urlRequest
/// Requet sender using default configuration.
let session = URLSession(configuration: .default)
/// Response JSON parser.
let decoder = JSONDecoder()
print("Tagging the string: \(testString)")
//: We want to examine the entire string.
let stringRange = testString.startIndex..<testString.endIndex
//: Create and set up the NLTagger instance that will categorize the words. We are only interested in the words,
//: not the punctuation or white spaces.
let tagger = NLTagger(tagSchemes: [.nameTypeOrLexicalClass])
tagger.string = testString
let allTags = tagger.tags(
in: stringRange,
unit: .word,
scheme: .nameTypeOrLexicalClass,
options: [.omitWhitespace, .omitPunctuation]
//: Filter the tags to find only the adjectives.
let adjectives = allTags.compactMap { (tag, range) -> ReplacementTarget? in
let tag = tag,
tag == .adjective
else { return nil }
return ReplacementTarget(value: String(testString[range]), range: range)
print("Found \(adjectives.count) adjective(s): \({ $0.value }.joined(separator: ", "))")
//: Pick a random adjective to send to the API for synonyms.
guard let selectedTarget = adjectives.randomElement() else {
fatalError("No adjectives found in string.")
//: Construct and send the URLRequest.
let query = selectedTarget.value
print("Executing API request for '\(query)'.")
let request = createRequest(for: query)
let task = session.dataTask(with: request) { (data, response, error) in
//: Whether the response is a success or failure, we want playground execution to finish once we're done here.
defer { PlaygroundPage.current.finishExecution() }
//: Ensure we received a response and not an error.
guard let data = data else {
if let error = error {
print("ERROR: \(error.localizedDescription)")
} else {
print("ERROR: Query failed for an unknown reason.")
//: Attempt to decode the response.
do {
let response = try decoder.decode([ThesaurusResponse].self, from: data)
let synonyms = response.first?.meta.synonyms.first,
else {
print("No synonyms returned from the API.")
//: Until such time as we do extra work to at least ensure that we are picking the correct part-of-speech
//: ("light" as a verb or a noun, for instance), we're just picking the first synonym set and picking a
//: random replacement.
print("Found \(synonyms.count) synonyms: \(synonyms.joined(separator: ", "))")
guard let substitution = synonyms.randomElement() else {
fatalError("Unable to pick a substitution.")
print("Picked '\(substitution)' as a substitution.")
let modifiedString = testString.replacingCharacters(in: selectedTarget.range, with: substitution)
print("Modified string: \(modifiedString)")
} catch {
print("Failed to decode the API response: \(error.localizedDescription)")
print("\(String(data: data, encoding: .utf8)!)")
//: Send the request to the API.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment