OAuth2: How to do Authentication with Unsplash API - ios

I'm writing an iOS app to using Unsplash API. And I get below issue when dealing with authentication workflow(login via website):
Following Authorization workflow on Unsplash API, I could get authorization code on the redirect page.
1) What I opened: https://unsplash.com/oauth/authorize?client_id={myclientid}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&scope=public
2) Safari get to the response page with Authorization code
3) Then I need to make a POST request to https://unsplash.com/oauth/token and include the Authorization code
/ / / My question is how to get the Authorization code in Step 2 with Swift?, I could deal with JSON response, but this is a page view in return, what class or method I should use to retrieve the code value?
/ / / Well, here is my code
class FancyClient {
static let clientID = "clientID"
static let clientSceret = "clientSceret"
struct Auth {
static var accessToken = ""
static var authorizationCode = ""
}
enum Endpoints {
static let base = "https://unsplash.com"
static let apiKeyParam = "?client_id=\(FancyClient.clientID)"
static let apiSecretParam = "?client_secret=\(FancyClient.clientSceret)"
case getAuthorizationCode
case login
case getAccessToken
var stringValue: String {
switch self {
case .getAuthorizationCode:
return Endpoints.base + "/oauth/authorize" + Endpoints.apiKeyParam + "&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob" + "&response_type=code" + "&scope=public"
case .getAccessToken:
return Endpoints.base + "/oauth/token" + Endpoints.apiKeyParam + "&\(Endpoints.apiSecretParam)" + "&redirect_uri=fancyimage:authenticate" + "&code=\(Auth.authorizationCode)" + "grant_type=Value authorization_code"
case .login:
return ""
}
}
var url: URL {
return URL(string: stringValue)!
}
}
class func getAccessToken(url: URL, completion: #escaping (Bool, Error?) -> Void ) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {
DispatchQueue.main.async {
completion(false, error)
}
return
}
do {
let decoder = JSONDecoder()
let responseObject = try decoder.decode(TokenResponse.self, from: data)
Auth.accessToken = responseObject.accessToken
DispatchQueue.main.async {
completion(true, nil)
}
} catch {
DispatchQueue.main.async {
completion(false, error)
}
}
}
task.resume()
}
}
class LoginViewController: UIViewController {
#IBOutlet weak var loginViaWeb: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func loginViaWebsiteTapped(_ sender: Any) {
print(FancyClient.Endpoints.getAuthorizationCode.url)
UIApplication.shared.open(FancyClient.Endpoints.getAuthorizationCode.url, options: [:], completionHandler: nil)
// if let authorizationCode = responsePage.authorizationCode {
FancyClient.getAccessToken(url: FancyClient.Endpoints.getAccessToken.url) { (success, error) in
// continue...
}
}
}
}

Related

How to use dependency injection networking with MVVM iOS?

I'm currently trying to find out what's the best networking architecture for MVVM applications. I couldn't find many resources and decided to go with dependency injection based architecture as per the very less reading resources that I have found.
I'm not using any 3rd party for web service testing and whatever the networking architecture that I use should be supported to mock the web services as well.
I have found out the DI based networking architecture which was build intended to achieve unit testing according to the Test Pyramid concept at Apple WWDC 2018.
So I have build my networking layer according to that. Following is my APIHandler class.
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
protocol RequestHandler {
associatedtype RequestDataType
func makeRequest(from data:RequestDataType) -> URLRequest?
}
protocol ResponseHandler {
associatedtype ResponseDataType
func parseResponse(data: Data, response: HTTPURLResponse) throws -> ResponseDataType
}
typealias APIHandler = RequestHandler & ResponseHandler
Followings are my extensions for request handler and response handler.
extension RequestHandler {
func setQueryParams(parameters:[String: Any], url: URL) -> URL {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.queryItems = parameters.map { element in URLQueryItem(name: element.key, value: String(describing: element.value) ) }
return components?.url ?? url
}
func setDefaultHeaders(request: inout URLRequest) {
request.setValue(APIHeaders.contentTypeValue, forHTTPHeaderField: APIHeaders.kContentType)
}
}
struct ServiceError: Error,Decodable {
let httpStatus: Int
let message: String
}
extension ResponseHandler {
func defaultParseResponse<T: Decodable>(data: Data, response: HTTPURLResponse) throws -> T {
let jsonDecoder = JSONDecoder()
if response.statusCode == 200 {
do {
let body = try jsonDecoder.decode(T.self, from: data)
return body
} catch {
throw ServiceError(httpStatus: response.statusCode, message: error.localizedDescription)
}
} else {
var message = "Generel.Message.Error".localized()
do {
let body = try jsonDecoder.decode(APIError.self, from: data)
if let err = body.fault?.faultstring {
message = err
}
} catch {
throw ServiceError(httpStatus: response.statusCode, message: error.localizedDescription)
}
throw ServiceError(httpStatus: response.statusCode, message:message)
}
}
}
Then I loaded my request using APILoader as follows.
struct APILoader<T: APIHandler> {
var apiHandler: T
var urlSession: URLSession
init(apiHandler: T, urlSession: URLSession = .shared) {
self.apiHandler = apiHandler
self.urlSession = urlSession
}
func loadAPIRequest(requestData: T.RequestDataType, completionHandler: #escaping (Int, T.ResponseDataType?, ServiceError?) -> ()) {
if let urlRequest = apiHandler.makeRequest(from: requestData) {
urlSession.dataTask(with: urlRequest) { (data, response, error) in
if let httpResponse = response as? HTTPURLResponse {
guard error == nil else {
completionHandler(httpResponse.statusCode, nil, ServiceError(httpStatus: httpResponse.statusCode, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
guard let responseData = data else {
completionHandler(httpResponse.statusCode,nil, ServiceError(httpStatus: httpResponse.statusCode, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
do {
let parsedResponse = try self.apiHandler.parseResponse(data: responseData, response: httpResponse)
completionHandler(httpResponse.statusCode, parsedResponse, nil)
} catch {
completionHandler(httpResponse.statusCode, nil, ServiceError(httpStatus: httpResponse.statusCode, message: CommonUtil.shared.decodeError(err: error)))
}
} else {
guard error == nil else {
completionHandler(-1, nil, ServiceError(httpStatus: -1, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
completionHandler(-1, nil, ServiceError(httpStatus: -1, message: "General.Error.Unknown".localized()))
}
}.resume()
}
}
}
To call my API request. I have created a separate service class and call the web service as follows.
struct TopStoriesAPI: APIHandler {
func makeRequest(from param: [String: Any]) -> URLRequest? {
let urlString = APIPath().topStories
if var url = URL(string: urlString) {
if param.count > 0 {
url = setQueryParams(parameters: param, url: url)
}
var urlRequest = URLRequest(url: url)
setDefaultHeaders(request: &urlRequest)
urlRequest.httpMethod = HTTPMethod.get.rawValue
return urlRequest
}
return nil
}
func parseResponse(data: Data, response: HTTPURLResponse) throws -> StoriesResponse {
return try defaultParseResponse(data: data,response: response)
}
}
For syncing both my actual web service methods and mock services, I have created an API Client protocol like follows.
protocol APIClientProtocol {
func fetchTopStories(completion: #escaping (StoriesResponse?, ServiceError?) -> ())
}
Then I have derived APIServices class using my APIClient protocol and implemented my all the APIs there by passing requests and responses. My dependency injection was getting over at this point.
public class APIServices: APIClientProtocol {
func fetchTopStories(completion: #escaping (StoriesResponse?, ServiceError?) -> ()) {
let request = TopStoriesAPI()
let params = [Params.kApiKey.rawValue : CommonUtil.shared.NytApiKey()]
let apiLoader = APILoader(apiHandler: request)
apiLoader.loadAPIRequest(requestData: params) { (status, model, error) in
if let _ = error {
completion(nil, error)
} else {
completion(model, nil)
}
}
}
}
Then I have called this API request on my viewModel class like this.
func fetchTopStories(completion: #escaping (Bool) -> ()) {
APIServices().fetchTopStories { response, error in
if let _ = error {
self.errorMsg = error?.message ?? "Generel.Message.Error".localized()
completion(false)
} else {
if let data = response?.results {
if data.count > 0 {
self.stories.removeAll()
self.stories = data
completion(true)
} else {
self.errorMsg = "Generel.NoData.Error".localized()
completion(false)
}
} else {
self.errorMsg = "Generel.NoData.Error".localized()
completion(false)
}
}
}
}
Finally call the viewModel's API call from my viewController (View).
func fetchData() {
showActivityIndicator()
self.viewModel.fetchTopStories { success in
self.hideActivityIndicator()
DispatchQueue.main.async {
if self.pullToRefresh {
self.pullToRefresh = false
self.refreshControl.endRefreshing()
}
if success {
if self.imgNoData != nil {
self.imgNoData?.isHidden = true
}
self.tableView.reloadData()
} else {
CommonUtil.shared.showToast(message: self.viewModel.errorMsg, success: false)
self.imgNoData = {
let viewWidth = self.tableView.frame.size.width
let imageWidth = viewWidth - 50
let iv = UIImageView()
iv.frame = CGRect(x: 25, y: 100, width: imageWidth, height: imageWidth)
iv.image = UIImage(named:"no-data")
iv.contentMode = .scaleAspectFit
return iv
}()
self.imgNoData?.isHidden = false
self.tableView.addSubview(self.imgNoData!)
}
}
}
}
So I have following questions regarding this approach.
I have ended the dependency injection from my APIServices class.
Should I bring this all the way up to my viewController class API Call and
pass request and params variables from there ?
Are there any performance issues in this approach and any
improvement to be done?
My personal preference is to end all the data related stuffs from the viewModel level and just call the API without passing any parameters from the viewController. Does it wrong? If we pass parameters from the view controller class as per the pure dependency injection way, does it harm to the MVVM architecture?

Swift: Order of execution, rest API

I need some help with my school project.
Why is print(segueShouldOccur) printed before doAPI().
When I actually call doApi() before the print(seagueShouldOccur).
I'm talking about the method: shouldPerformSegue.
The Rest Api does work (already tested).
class ViewController: UIViewController {
var loginArr = [String]()
#IBOutlet weak var _output: UILabel!
#IBOutlet weak var _username: UITextField!
#IBOutlet weak var _password: UITextField!
#IBAction func doLogin(_ sender: Any) {
loginArr.removeAll()
let username = _username.text;
let password = _password.text;
loginArr.append(username!);
loginArr.append(password!);
self._output.text = username;
}
func doApi() -> Bool{
let headers = [
"cache-control": "no-cache",
"postman-token": "6f8a-12c6-87a1-ac0f25d6385a"
]
let url = "https://projects2018.sz-ybbs.ac.at/~szmed/indyapp/indyapi.php?func=0&user=" + _username.text! + "&pass=" + _password.text!
let request = NSMutableURLRequest(url: NSURL(string: url)! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
var check = false;
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if error == nil && data != nil {
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! [String:AnyObject]
//do your stuff
print(json);
check = true;
} catch {
}
}
else if error != nil
{
}
}).resume()
return check;
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let DashboardC = segue.destination as! DashboardController
DashboardC.receivedStringArr = loginArr
}
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "performSegueLogin" { // you define it in the storyboard (click on the segue, then Attributes' inspector > Identifier
var segueShouldOccur = doApi()
if (!segueShouldOccur){
print("1 - false");
print(segueShouldOccur);
return false;
}else{
print("2 - true");
print(segueShouldOccur);
return true;
}
}
return false;
}
}
session.dataTask is asynchronous. If you want to know when your api call has completed, you can use completion handler like this :
func doApi(completion : #escaping (Bool?,Error?) -> ()) {
let headers = [
"cache-control": "no-cache",
"postman-token": "6f8a-12c6-87a1-ac0f25d6385a"
]
let url = "https://projects2018.sz-ybbs.ac.at/~szmed/indyapp/indyapi.php?func=0&user=" + _username.text! + "&pass=" + _password.text!
let request = NSMutableURLRequest(url: NSURL(string: url)! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
var check = false;
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if error == nil && data != nil {
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! [String:AnyObject]
//do your stuff
print(json);
check = true;
} catch {
}
completion(true,nil)
}
else if error != nil
{
completion(false,error)
}
}).resume()
return check;
}
then you can call your function like this :
doApi { (isSuccess, errorMessage) in
if isSuccess {
// perform your operations
} else {
print(errorMessage?.localizedDescription ?? "Some error occured")
}
}

Alamofire is getting slow on HTTPS but fine in HTTP

In my App multiple Apis . i have 2 differnet server one is for HTTP and one for HTTPS
When i Run my App on Http : it works fine 1st time for each Api and 2 second time for each Api same response time .
But when i run App on https : for each APi first time taking extra time for each Api , then if i hit same Api again it is fast . Problem is why for first time for each APi is slow or taking extra time . But thing is not happened with Android App .
here is My Request builder Class : URLRequestBuilder.swift
import Foundation
import Alamofire
protocol URLRequestBuilder: URLRequestConvertible, APIRequestHandler {
var mainURL: URL { get }
var requestURL: URL { get }
var path: String { get }
var parameters: Parameters? { get }
var method: HTTPMethod { get }
var encoding: ParameterEncoding { get }
var urlRequest: URLRequest { get }
}
extension URLRequestBuilder {
var encoding: ParameterEncoding {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}
var mainURL: URL {
return URL(string: SERVER_URL)!
}
var requestURL: URL {
var fullURL = mainURL.absoluteString + path
if L102Language.currentAppleLanguage() == "ar" && path.contains("?") {
fullURL = fullURL + "&blLocaleCode=ar"
} else if L102Language.currentAppleLanguage() == "ar" {
fullURL = fullURL + "?blLocaleCode=ar"
}
let urlComponents = URLComponents(string: fullURL)!
return urlComponents.url!
}
var urlRequest: URLRequest {
var request = URLRequest(url: requestURL)
request.httpMethod = method.rawValue
if UserDefaults.standard.isUserLoggedIn() {
request.setValue(UserDefaults.standard.getAccessToken(), forHTTPHeaderField: NETWORK_ACCESS_TOKEN)
}
request.setValue(NETWORK_REQUEST_TYPE, forHTTPHeaderField: NETWORK_ACCEPT)
request.setValue(NETWORK_REQUEST_TYPE, forHTTPHeaderField: NETWORK_CONTENT_TYPE)
//request.cachePolicy = .useProtocolCachePolicy
return request
}
func asURLRequest() throws -> URLRequest {
return try encoding.encode(urlRequest, with: parameters)
}
}
final class NetworkClient {
let evaluators = [
"somehttpsURL.com": ServerTrustPolicy.pinCertificates(
certificates: [Certificates.stackExchange],
validateCertificateChain: true,
validateHost: true)
]
let session: SessionManager
// 2
private init() {
session = SessionManager(
serverTrustPolicyManager: ServerTrustPolicyManager(policies: evaluators))
}
// MARK: - Static Definitions
private static let shared = NetworkClient()
static func request(_ convertible: URLRequestConvertible) -> DataRequest {
return shared.session.request(convertible)
}
}
struct Certificates {
static let stackExchange =
Certificates.certificate(filename: "certificate")
private static func certificate(filename: String) -> SecCertificate {
let filePath = Bundle.main.path(forResource: filename, ofType: "der")!
let data = try? Data(contentsOf: URL(fileURLWithPath: filePath))
let certificate = SecCertificateCreateWithData(nil, data! as CFData)!
return certificate
}
}
And my Request Handler Class is :
extension APIRequestHandler where Self : URLRequestBuilder {
// For Response Object
func send<T: AnyObject>(modelType: T.Type, data: [UIImage]? = nil, success: #escaping ( _ servicResponse: AnyObject) -> Void, fail: #escaping ( _ error: NSError) -> Void, showHUD: Bool) where T: Mappable {
if let data = data {
uploadToServerWith(modelType: modelType, images: data, request: self, parameters: self.parameters, success: success, fail: fail)
} else {
//print(requestURL.absoluteString)
// NetworkClient.
request(self).authenticate(user: APIAuthencationUserName, password: APIAuthencationPassword).validate().responseObject { (response: DataResponse<T>) in
switch response.result {
case .success(let objectData):
success(objectData)
break
case .failure(let error):
print(error.localizedDescription)
if error.localizedDescription == RefreshTokenFailed {
self.getAccessTokenAPI(completion: { (value) in
if value == TOKEN_SAVED {
self.send(modelType: modelType, success: success, fail: fail, showHUD: showHUD)
return
}else {
fail(error as NSError)
}
})
} else {
fail(error as NSError)
}
}
}
}
}
}

Codable for API request

How would I make this same API request through codables?
In my app, this function is repeated in every view that makes API calls.
func getOrders() {
DispatchQueue.main.async {
let spinningHUD = MBProgressHUD.showAdded(to: self.view, animated: true)
spinningHUD.isUserInteractionEnabled = false
let returnAccessToken: String? = UserDefaults.standard.object(forKey: "accessToken") as? String
let access = returnAccessToken!
let headers = [
"postman-token": "dded3e97-77a5-5632-93b7-dec77d26ba99",
"Authorization": "JWT \(access)"
]
let request = NSMutableURLRequest(url: NSURL(string: "https://somelink.com")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error!)
} else {
if let dataNew = data, let responseString = String(data: dataNew, encoding: .utf8) {
print("----- Orders -----")
print(responseString)
print("----------")
let dict = self.convertToDictionary(text: responseString)
print(dict?["results"] as Any)
guard let results = dict?["results"] as? NSArray else { return }
self.responseArray = (results) as! [HomeVCDataSource.JSONDictionary]
DispatchQueue.main.async {
spinningHUD.hide(animated: true)
self.tableView.reloadData()
}
}
}
})
dataTask.resume()
}
}
I would suggest to do the following
Create Base Service as below
import UIKit
import Foundation
enum MethodType: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
class BaseService {
var session: URLSession!
// MARK: Rebuilt Methods
func FireGenericRequest<ResponseModel: Codable>(url: String, methodType: MethodType, headers: [String: String]?, completion: #escaping ((ResponseModel?) -> Void)) {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
// Request Preparation
guard let serviceUrl = URL(string: url) else {
print("Error Building URL Object")
return
}
var request = URLRequest(url: serviceUrl)
request.httpMethod = methodType.rawValue
// Header Preparation
if let header = headers {
for (key, value) in header {
request.setValue(value, forHTTPHeaderField: key)
}
}
// Firing the request
session = URLSession(configuration: URLSessionConfiguration.default)
session.dataTask(with: request) { (data, response, error) in
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
if let data = data {
do {
guard let object = try? JSONDecoder().decode(ResponseModel.self , from: data) else {
print("Error Decoding Response Model Object")
return
}
DispatchQueue.main.async {
completion(object)
}
}
}
}.resume()
}
private func buildGenericParameterFrom<RequestModel: Codable>(model: RequestModel?) -> [String : AnyObject]? {
var object: [String : AnyObject] = [String : AnyObject]()
do {
if let dataFromObject = try? JSONEncoder().encode(model) {
object = try JSONSerialization.jsonObject(with: dataFromObject, options: []) as! [String : AnyObject]
}
} catch (let error) {
print("\nError Encoding Parameter Model Object \n \(error.localizedDescription)\n")
}
return object
}
}
the above class you may reuse it in different scenarios adding request object to it and passing any class you would like as long as you are conforming to Coddle protocol
Create Model Conforming to Coddle protocol
class ExampleModel: Codable {
var commentId : String?
var content : String?
//if your JSON keys are different than your property name
enum CodingKeys: String, CodingKey {
case commentId = "CommentId"
case content = "Content"
}
}
Create Service to the specific model with the endpoint constants subclassing to BaseService as below
class ExampleModelService: BaseService<ExampleModel/* or [ExampleModel]*/> {
func GetExampleModelList(completion: ((ExampleModel?)/* or [ExampleModel]*/ -> Void)?) {
super.FireRequestWithURLSession(url: /* url here */, methodType: /* method type here */, headers: /* headers here */) { (responseModel) in
completion?(responseModel)
}
}
}
Usage
class MyLocationsController: UIViewController {
// MARK: Properties
// better to have in base class for the controller
var exampleModelService: ExampleModelService = ExampleModelService()
// MARK: Life Cycle Methods
override func viewDidLoad() {
super.viewDidLoad()
exampleModelService.GetExampleModelList(completion: { [weak self] (response) in
// model available here
})
}
}
Basically, you need to conform Codable protocol in your model classes, for this you need to implement 2 methods, one for code your model and another for decode your model from JSON
func encode(to encoder: Encoder) throws
required convenience init(from decoder: Decoder) throws
After that you will be able to use JSONDecoder class provided by apple to decode your JSON, and return an array (if were the case) or an object of your model class.
class ExampleModel: Codable {
var commentId : String?
var content : String?
//if your JSON keys are different than your property name
enum CodingKeys: String, CodingKey {
case commentId = "CommentId"
case content = "Content"
}
}
Then using JSONDecoder you can get your model array like this
do {
var arrayOfOrders : [ExampleModel] = try JSONDecoder().decode([ExampleModel].self, from: dataNew)
}
catch {
}
First of all, I can recommend you to use this application -quicktype- for turning json file to class or struct (codable) whatever you want. enter link description here.
After that you can create a generic function to get any kind of codable class and return that as a response.
func taskHandler<T:Codable>(type: T.Type, useCache: Bool, urlRequest: URLRequest, completion: #escaping (Result<T, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print("error : \(error)")
}
if let data = data {
do {
let dataDecoded = try JSONDecoder().decode(T.self, from: data)
completion(.success(dataDecoded))
// if says use cache, let's store response data to cache
if useCache {
if let response = response as? HTTPURLResponse {
self.storeDataToCache(urlResponse: response, urlRequest: urlRequest, data: data)
}
}
} catch let error {
completion(.failure(error))
}
} else {
completion(.failure(SomeError))
}
}
task.resume()
}

Get full URL from short URL in Swift on iOS

Given a short URL https://itun.es/us/JB7h_, How do you expand it into the full URL? e.g. https://music.apple.com/us/album/blackstar/1059043043
Extension
extension URL {
func getExpandedURL() async throws -> Result<URL, Error> {
var request = URLRequest(url: self)
request.httpMethod = "HEAD"
let (_, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
throw URLError.unableToExpand
}
if let expandedURL = response.url {
return .success(expandedURL)
} else {
throw URLError.unableToExpand
}
}
enum URLError: Error {
case unableToExpand
}
}
Crude Demo
struct ContentView: View {
let shortURL = URL(string: "https://itun.es/us/JB7h_")
#State var expandedURLResult: Result<URL, Error>?
var body: some View {
Form {
Section("Short URL") {
Text(shortURL?.description ?? "")
}
Section("Long URL") {
switch expandedURLResult {
case .some(.success(let expandedURL)):
Text(expandedURL.description)
case .none:
Text("Waiting")
case .some(.failure(let error)):
Text(error.localizedDescription)
}
}
}
.task {
do {
expandedURLResult = try await shortURL?.getExpandedURL()
} catch {
expandedURLResult = .failure(error)
}
}
}
}
The final resolved URL will be returned to you in the NSURLResponse: response.URL.
You should also make sure to use the HTTP HEAD method to avoid downloading unnecessary data (since you don't care about the resource body).
Swift 4.2 Updated :
extension URL {
func resolveWithCompletionHandler(completion: #escaping (URL) -> Void) {
let originalURL = self
var req = URLRequest(url: originalURL)
req.httpMethod = "HEAD"
URLSession.shared.dataTask(with: req) { body, response, error in
completion(response?.url ?? originalURL)
}.resume()
}
Older Swift Versions:
extension NSURL
{
func resolveWithCompletionHandler(completion: NSURL -> Void)
{
let originalURL = self
let req = NSMutableURLRequest(URL: originalURL)
req.HTTPMethod = "HEAD"
NSURLSession.sharedSession().dataTaskWithRequest(req) { body, response, error in
completion(response?.URL ?? originalURL)
}.resume()
}
}
// Example:
NSURL(string: "https://itun.es/us/JB7h_")!.resolveWithCompletionHandler {
print("resolved to \($0)") // prints https://itunes.apple.com/us/album/blackstar/id1059043043
}

Resources