I want to offer a more modern solution using DispatchGroup.
Usage example 1:
var urlRequest = URLRequest(url: config.pullUpdatesURL)
urlRequest.httpMethod = "GET"
urlRequest.httpBody = requestData
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, response, error) = URLSession.shared.syncRequest(with: urlRequest)
Usage example 2:
let url = URL(string: "https://www.google.com/")
let (data, response, error) = URLSession.shared.syncRequest(with: url)
Extension code:
extension URLSession {
   
   func syncRequest(with url: URL) -> (Data?, URLResponse?, Error?) {
      var data: Data?
      var response: URLResponse?
      var error: Error?
      
      let dispatchGroup = DispatchGroup()
      let task = dataTask(with: url) {
         data = $0
         response = $1
         error = $2
         dispatchGroup.leave()
      }
      dispatchGroup.enter()
      task.resume()
      dispatchGroup.wait()
      
      return (data, response, error)
   }
   
   func syncRequest(with request: URLRequest) -> (Data?, URLResponse?, Error?) {
      var data: Data?
      var response: URLResponse?
      var error: Error?
      
      let dispatchGroup = DispatchGroup()
      let task = dataTask(with: request) {
         data = $0
         response = $1
         error = $2
         dispatchGroup.leave()
      }
      dispatchGroup.enter()
      task.resume()
      dispatchGroup.wait()
      
      return (data, response, error)
   }
   
}
As a bonus, if you need to, you can easily implement a timeout. To do this, you need to use
func wait(timeout: DispatchTime) -> DispatchTimeoutResult instead of
func wait()