Why does webSocketTask.receive never complete and how do I force completion in my Swift app? - swift

I have a Swift app that uses a web socket to download stock price information from a public API. I send a message through the socket to subscribe to various stock price changes then wait for a continuous stream of messages to be received but when I turn off wifi the call to the message receive function, webSocketTask.receive, never returns. How can I force abort of the message receive function so that I alert the user that the network connection has been lost.
Here is my NetworkServices class;
import UIKit
import Network
protocol NetworkServicesDelegate: AnyObject {
func sendStockInfo(stocksInfo: [String: StockInfo])
}
final class NetworkServices: NSObject {
static let sharedInstance = NetworkServices()
var urlSession: URLSession?
var webSocketTask: URLSessionWebSocketTask?
var stocksInfo: [String: StockInfo] = [:]
var socketResults: [String: [StockInfo]] = [:]
weak var delegate: NetworkServicesDelegate?
var timer: Timer?
var stockSymbols: [String] = []
private let queue = DispatchQueue.global(qos: .background)
private let monitor = NWPathMonitor()
public private(set) var isConnected: Bool = false
public private(set) var connectionType: ConnectionType = .unknown
enum ConnectionType {
case wifi
case cellular
case wiredEthernet
case unknown
}
public func startMonitoring() {
monitor.start(queue: queue)
monitor.pathUpdateHandler = { path in
self.isConnected = path.status == .satisfied
self.getConnectionType(path)
print("DEBUG: isConnected = \(self.isConnected); connectionType = \(self.connectionType)")
if self.isConnected == false {
}
}
}
public func stopMonitoring() {
monitor.cancel()
}
private func getConnectionType(_ path: NWPath) {
if path.usesInterfaceType(.wifi) {
connectionType = .wifi
} else if path.usesInterfaceType(.cellular) {
connectionType = .cellular
} else if path.usesInterfaceType(.wiredEthernet) {
connectionType = .wiredEthernet
} else {
connectionType = .unknown
}
}
func fetchStockInfo(symbols: [String], delegate: CompanyPriceListVC) {
stockSymbols = symbols
self.delegate = delegate
let configuration = URLSessionConfiguration.default
configuration.waitsForConnectivity = false
urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)
webSocketTask = urlSession?.webSocketTask(with: FINNHUB_SOCKET_STOCK_INFO_URL!)
webSocketTask?.delegate = self
webSocketTask!.resume()
for symbol in symbols {
let string = FINNHUB_SOCKET_MESSAGE_STRING + symbol + "\"}"
let message = URLSessionWebSocketTask.Message.string(string)
if isConnected {
webSocketTask!.send(message) { error in
if let error = error {
print("DEBUG: Error sending message: \(error)")
}
self.receiveMessage()
}
} else {
// Post notification to view controllers that connection has been lost
let name = Notification.Name(rawValue: isNotConnectedNotificationKey)
NotificationCenter.default.post(name: name, object: nil)
// try to re-connect
// successful re-connect?
// No, keep trying. Yes, call send message method again
}
}
}
private func receiveMessage() {
if isConnected {
self.webSocketTask?.receive { result in
print("DEBUG: Inside closure.")
switch result {
case .failure(let error):
print("DEBUG: Error receiving message: \(error.localizedDescription)")
case .success(.string(let jsonData)):
guard let stockData = jsonData.data(using: .utf8) else { return }
self.socketResults = [:]
self.stocksInfo = [:]
let decoder = JSONDecoder()
do {
let socketData = try decoder.decode(SocketData.self, from: stockData)
guard let stockInfoData = socketData.data else { return }
for stockInfo in stockInfoData {
let symbol = stockInfo.symbol
if self.socketResults[symbol] == nil {
self.socketResults[symbol] = [StockInfo]()
}
self.socketResults[symbol]?.append(stockInfo)
}
for (symbol, stocks) in self.socketResults {
for item in stocks {
if self.stocksInfo[symbol] == nil {
self.stocksInfo[symbol] = item
} else if item.timestamp > self.stocksInfo[symbol]!.timestamp {
self.stocksInfo[symbol] = item
}
}
}
self.delegate?.sendStockInfo(stocksInfo: self.stocksInfo)
self.receiveMessage()
} catch {
print("DEBUG: Error converting JSON: \(error)")
}
default:
print("DEBUG: default")
}
}
print("DEBUG: Got here 1")
} else {
print("DEBUG: Got here 2")
// Post notification to view controllers that connection has been lost
let name = Notification.Name(rawValue: isNotConnectedNotificationKey)
NotificationCenter.default.post(name: name, object: nil)
// try to re-connect.
// successful reconnect?
// No, keep trying. Yes, call receive message method again.
}
}
func closeWebSocketConnection() {
webSocketTask?.cancel(with: .goingAway, reason: nil)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("DEBUG: didCompleteWithError called: error = \(error)")
}
}
func fetchCompanyInfo(symbol: String, completion: #escaping (CompanyInfo?, UIImage?)->()) {
let urlString = FINNHUB_HTTP_COMPANY_INFO_URL_STRING + symbol + "&token=" + FINNHUB_API_TOKEN
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error fetching company info: \(error)")
}
guard let data = data else { return }
let decoder = JSONDecoder()
do {
let companyInfo = try decoder.decode(CompanyInfo.self, from: data)
guard let logoURL = URL(string: companyInfo.logo) else { return }
let task = URLSession.shared.dataTask(with: logoURL) { data, response, error in
if let error = error {
print("Error fetching logo image: \(error)")
}
guard let data = data else { return }
guard let logoImage = UIImage(data: data) else { return }
completion(companyInfo, logoImage)
}
task.resume()
} catch {
print("Error decoding JSON: \(error)")
completion(nil, nil)
}
}
task.resume()
}
}
extension NetworkServices: URLSessionTaskDelegate, URLSessionWebSocketDelegate, URLSessionDelegate {
func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
print("DEBUG: inside taskIsWaitingForConnectivity")
}
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
print("DEBUG: didBecomeInvalidWithError: error = \(String(describing: error?.localizedDescription))")
}
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
let reasonString: String
if let reason = reason, let string = String(data: reason, encoding: .utf8) {
reasonString = string
} else {
reasonString = ""
}
print("DEBUG: didCloseWith called: close code is \(closeCode), reason is \(String(describing: reasonString))")
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
print("DEBUG: urlSessionDidFinishEvents called")
}
}
I have tried placing code in the pathUpdateHandler closure to cancel the task on the task object and finish current tasks and cancel on the URLSession object but neither works.

I needed to post a notification when path.status == .satisfied. The notification goes to the custom UIViewController class which then calls the FetchStockInfo(symbols:delegate) method.

Related

How do I handle web socket re-connect for .cancelled case in Starscream pod?

I have a small iOS app in Swift which fetches data on stock prices from a public API using the Starscream swift pod running on an iPhone Pro 12. The app downloads, processes and displays the stock data correctly when wifi is connected. However, if I swipe up from the bottom of the device, the app screen disappears into the app icon and my debug window reports that sceneDidResignActive followed by sceneDidEnterBackground are called. The debug console also reports that the .cancelled case is being sent to the didReceive(event:client:) delegate method of StarScream. When I swipe up from the bottom of the device again and select the app screen the scene delegate methods sceneWillEnterForeground and sceneDidBecomeActive are called but my app is no longer updating and displaying live stock price information. The socket property is still there but is not connected. I have tried placing the line self.socket.connect() in the .cancelled switch case but it stops my app from handling wifi disconnection correctly. What code should I have in my scene delegate file or in .cancelled switch case to get the app to continue downloading and displaying stock price information from the API?
Here is my NetworkServices class;
//
// Services.swift
// BetVictorTask
//
// Created by Stephen Learmonth on 28/02/2022.
//
import UIKit
import Starscream
import Network
protocol NetworkServicesDelegate: AnyObject {
func sendStockInfo(stocksInfo: [String: StockInfo])
}
final class NetworkServices {
static let sharedInstance = NetworkServices()
var request = URLRequest(url: FINNHUB_SOCKET_STOCK_INFO_URL!)
var socket: WebSocket!
public private(set) var isConnected = false
var stocksInfo: [String: StockInfo] = [:]
var socketResults: [String: [StockInfo]] = [:]
weak var delegate: NetworkServicesDelegate?
var stockSymbols: [String] = []
private init() {
request.timeoutInterval = 5
socket = WebSocket(request: request)
socket.delegate = self
}
private let queue = DispatchQueue.global()
private let monitor = NWPathMonitor()
public func startMonitoring() {
monitor.start(queue: queue)
self.monitor.pathUpdateHandler = { [weak self] path in
if path.status == .satisfied {
// connect the socket
self?.socket.connect()
print("DEBUG: socket is connected")
} else {
self?.socket.disconnect()
print("DEBUG: socket is disconnected")
self?.isConnected = false
// post notification that socket is now disconnected
DispatchQueue.main.async {
print("DEBUG: Notification \"isDisconnected\" posted")
let name = Notification.Name(rawValue: isDisconnectedNotificationKey)
NotificationCenter.default.post(name: name, object: nil)
}
}
}
}
public func stopMonitoring() {
monitor.cancel()
socket.disconnect()
isConnected = false
}
func fetchStockInfo(symbols: [String], delegate: CompanyPriceListVC) {
stockSymbols = symbols
self.delegate = delegate
for symbol in symbols {
let string = FINNHUB_SOCKET_MESSAGE_STRING + symbol + "\"}"
socket.write(string: string)
}
}
private func parseJSONSocketData(_ socketString: String) {
self.socketResults = [:]
self.stocksInfo = [:]
let decoder = JSONDecoder()
do {
let socketData = try decoder.decode(SocketData.self, from: socketString.data(using: .utf8)!)
guard let stockInfoData = socketData.data else { return }
for stockInfo in stockInfoData {
let symbol = stockInfo.symbol
if self.socketResults[symbol] == nil {
self.socketResults[symbol] = [StockInfo]()
}
self.socketResults[symbol]?.append(stockInfo)
}
for (symbol, stocks) in self.socketResults {
for item in stocks {
if self.stocksInfo[symbol] == nil {
self.stocksInfo[symbol] = item
} else if item.timestamp > self.stocksInfo[symbol]!.timestamp {
self.stocksInfo[symbol] = item
}
}
}
self.delegate?.sendStockInfo(stocksInfo: self.stocksInfo)
} catch {
print("DEBUG: error: \(error.localizedDescription)")
}
}
func fetchCompanyDetails(symbol: String, completion: #escaping (CompanyInfo?, UIImage?)->()) {
let urlString = FINNHUB_HTTP_COMPANY_INFO_URL_STRING + symbol + "&token=" + FINNHUB_API_TOKEN
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error fetching company info: \(error)")
}
guard let data = data else { return }
let decoder = JSONDecoder()
do {
let companyInfo = try decoder.decode(CompanyInfo.self, from: data)
guard let logoURL = URL(string: companyInfo.logo) else { return }
let task = URLSession.shared.dataTask(with: logoURL) { data, response, error in
if let error = error {
print("Error fetching logo image: \(error)")
}
guard let data = data else { return }
guard let logoImage = UIImage(data: data) else { return }
completion(companyInfo, logoImage)
}
task.resume()
} catch {
print("Error decoding JSON: \(error)")
completion(nil, nil)
}
}
task.resume()
}
}
extension NetworkServices: WebSocketDelegate {
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(_):
self.isConnected = true
DispatchQueue.main.async {
// post notification that socket is now connected
let name = Notification.Name(rawValue: isConnectedNotificationKey)
NotificationCenter.default.post(name: name, object: nil)
print("DEBUG: Notification \"isConnected\" posted")
}
case .disconnected(let reason, let code):
print("DEBUG: Got disconnected reason = \(reason) code = \(code)")
self.isConnected = false
case .cancelled:
print("DEBUG: cancelled.")
// socket = WebSocket(request: request)
// socket.delegate = self
// startMonitoring()
case .reconnectSuggested(let suggestReconnect):
// print("DEBUG: suggestReconnect = \(suggestReconnect)")
break
case .viabilityChanged(let viabilityChanged):
// print("DEBUG: viabilityChanged = \(viabilityChanged)")
break
case .error(let error):
print("DEBUG: error: \(String(describing: error?.localizedDescription))")
case .text(let socketString):
// print("DEBUG: .text available")
parseJSONSocketData(socketString)
default:
break
}
}
}

How do I detect loss of connection from a web socket in my Swift App

I am trying to detect loss of connection to my web socket in Swift. I have very little understanding of web sockets but I have tried googling for information but with little success. Here is my custom class providing a web socket service;
//
// Services.swift
// BetVictorTask
//
// Created by Stephen Learmonth on 28/02/2022.
//
import UIKit
protocol NetworkServicesDelegateProtocol: AnyObject {
func sendStockInfo(stocksInfo: [String: StockInfo])
}
class NetworkServices: NSObject, URLSessionWebSocketDelegate {
static let sharedInstance = NetworkServices()
var webSocketTask: URLSessionWebSocketTask?
var stocksInfo: [String: StockInfo] = [:]
var socketResults: [String: [StockInfo]] = [:]
weak var delegate: NetworkServicesDelegateProtocol?
func fetchStockInfo(symbols: [String], delegate: CompanyPriceListVC) {
self.delegate = delegate
let urlSession = URLSession(configuration: .default)
webSocketTask = urlSession.webSocketTask(with: FINNHUB_SOCKET_STOCK_INFO_URL!)
webSocketTask!.resume()
for symbol in symbols {
let string = FINNHUB_SOCKET_MESSAGE_STRING + symbol + "\"}"
let message = URLSessionWebSocketTask.Message.string(string)
webSocketTask!.send(message) { error in
if let error = error {
print("Error sending message: \(error)")
}
self.receiveMessage()
}
}
}
private func receiveMessage() {
self.webSocketTask!.receive { result in
switch result {
case .failure(let error):
print("Error receiving message: \(error)")
case .success(.string(let jsonData)):
guard let stockData = jsonData.data(using: .utf8) else { return }
self.socketResults = [:]
self.stocksInfo = [:]
let decoder = JSONDecoder()
do {
let socketData = try decoder.decode(SocketData.self, from: stockData)
guard let stockInfoData = socketData.data else { return }
for stockInfo in stockInfoData {
let symbol = stockInfo.symbol
if self.socketResults[symbol] == nil {
self.socketResults[symbol] = [StockInfo]()
}
self.socketResults[symbol]?.append(stockInfo)
}
for (symbol, stocks) in self.socketResults {
for item in stocks {
if self.stocksInfo[symbol] == nil {
self.stocksInfo[symbol] = item
} else if item.timestamp > self.stocksInfo[symbol]!.timestamp {
self.stocksInfo[symbol] = item
}
}
}
self.delegate?.sendStockInfo(stocksInfo: self.stocksInfo)
self.receiveMessage()
} catch {
print("Error converting JSON: \(error)")
}
default:
print("default")
}
}
}
func closeWebSocketConnection() {
webSocketTask?.cancel(with: .goingAway, reason: nil)
}
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
print("Got here")
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let error = error as? URLError else { return }
print("got here")
}
func fetchCompanyInfo(symbol: String, completion: #escaping (CompanyInfo?, UIImage?)->()) {
let urlString = FINNHUB_HTTP_COMPANY_INFO_URL_STRING + symbol + "&token=" + FINNHUB_API_TOKEN
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error fetching company info: \(error)")
}
guard let data = data else { return }
let decoder = JSONDecoder()
do {
let companyInfo = try decoder.decode(CompanyInfo.self, from: data)
guard let logoURL = URL(string: companyInfo.logo) else { return }
let task = URLSession.shared.dataTask(with: logoURL) { data, response, error in
if let error = error {
print("Error fetching logo image: \(error)")
}
guard let data = data else { return }
guard let logoImage = UIImage(data: data) else { return }
completion(companyInfo, logoImage)
}
task.resume()
} catch {
print("Error decoding JSON: \(error)")
completion(nil, nil)
}
}
task.resume()
}
}
I have tried implementing;
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
print("Got here")
}
and then run the app and turn off WiFi to simulate loss of connection but the urlSession method never gets called.
How can I simulate loss of web socket connection while running my app and detect this disconnection. Also I need to think of a way to reconnect the web socket

Show error response in alert RxSwift using Driver

How to get an error response with driver so I can show it in alert. When I see the trait driver is can't error out, so should I use subject or behaviourRelay to get error response when I subscribe. Actually I like how to use driver but I don't know how to passed error response using driver.
this is my network service
func getMovies(page: Int) -> Observable<[MovieItem]> {
return Observable.create { observer -> Disposable in
self.service.request(endpoint: .discover(page: page)) { data, response, error in
if let _ = error {
observer.onError(MDBError.unableToComplete)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
observer.onError(MDBError.invalidResponse)
return
}
guard let data = data else {
observer.onError(MDBError.invalidData)
return
}
if let decode = self.decode(jsonData: MovieResults.self, from: data) {
observer.onNext(decode.results)
}
observer.onCompleted()
}
return Disposables.create()
}
}
This is my viewModel
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let movies = input.viewDidLoad
.flatMap { _ in
self.service.getMovies(page: 1)
.trackActivity(loading)
.asDriver(onErrorJustReturn: [])
}
let errorResponse = movies
return Output(loading: loading.asDriver(),movies: movies)
}
}
this is how I bind the viewModel in viewController
let input = PopularViewModel.Input(viewDidLoad: rx.viewDidLoad.asDriver())
let output = viewModel.transform(input: input)
output.movies.drive { [weak self] movies in
guard let self = self else { return }
self.populars = movies
self.updateData(on: movies)
}.disposed(by: disposeBag)
output.loading
.drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
.disposed(by: disposeBag)
You do this the same way you handled the ActivityIndicator...
The ErrorRouter type below can be found here.
This is such a common pattern that I have created an API class that takes care of this automatically.
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
let displayAlertMessage: Driver<String>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let errorRouter = ErrorRouter()
let movies = input.viewDidLoad
.flatMap { [service] in
service.getMovies(page: 1)
.trackActivity(loading)
.rerouteError(errorRouter)
.asDriver(onErrorRecover: { _ in fatalError() })
}
let displayAlertMessage = errorRouter.error
.map { $0.localizedDescription }
.asDriver(onErrorRecover: { _ in fatalError() })
return Output(
loading: loading.isActive.asDriver(onErrorRecover: { _ in fatalError() }),
movies: movies,
displayAlertMessage: displayAlertMessage
)
}
}

#Published is not getting updated, State problem? - SwiftUI

Right now I have to call the function (calculatePortfolioGrossBalance) 3 times for the value to update, what am I doing wrong in the state logic?
In the code below, when I call in an init the function calculatePortfolioGrossBalance() it returns empty [], I have to call it 3 times for the value to update, However... if I print the values of getTokenBalancesModel in the line DispatchQueue.main.async { I can see the values are there, so how come in calculatePortfolioGrossBalance are not?
final class TokenBalancesClassAViewModel: ObservableObject {
#Published var getTokenBalancesModel: [TokenBalancesItemsModel] = [TokenBalancesItemsModel]()
#Published var portfolioGrossBalance: String = "0.0"
func calculatePortfolioGrossBalance() {
getTokenBalances()
DispatchQueue.main.async {
var totalBalance: Double = 0
for item in self.getTokenBalancesModel {
totalBalance += Double(item.quote!)
}
self.portfolioGrossBalance = String(format:"%.2f", totalBalance)
print(self.portfolioGrossBalance)
}
}
func getTokenBalances() {
guard let url = URL(string: "someUrlHeidiGaveMe") else {
print("Invalid URL")
return
}
print("Calling getTokenBalances() ...")
AF.request(url, method: .get).validate().responseData(completionHandler: { data in
do {
guard let data = data.data else {
print("Response Error:", data.error as Any)
return
}
let apiJsonData = try JSONDecoder().decode(TokenBalancesModel.self, from: data)
DispatchQueue.main.async {
self.getTokenBalancesModel = apiJsonData.data.items
}
} catch {
print("ERROR:", error)
}
})
}
}
you need to read up on using asynchronous functions, how to set them up and how to use them. This is important. Try something like this (untested):
final class TokenBalancesClassAViewModel: ObservableObject {
#Published var getTokenBalancesModel: [TokenBalancesItemsModel] = [TokenBalancesItemsModel]()
#Published var portfolioGrossBalance: String = "0.0"
func calculatePortfolioGrossBalance() {
getTokenBalances() { isGood in
if isGood {
var totalBalance: Double = 0
for item in self.getTokenBalancesModel {
totalBalance += Double(item.quote!)
}
self.portfolioGrossBalance = String(format:"%.2f", totalBalance)
print(self.portfolioGrossBalance)
}
}
}
func getTokenBalances(completion: #escaping (Bool) -> Void) {
guard let url = URL(string: "someUrlHeidiGaveMe") else {
print("Invalid URL")
completion(false)
return
}
print("Calling getTokenBalances() ...")
AF.request(url, method: .get).validate().responseData(completionHandler: { data in
do {
guard let data = data.data else {
print("Response Error:", data.error as Any)
completion(false)
return
}
let apiJsonData = try JSONDecoder().decode(TokenBalancesModel.self, from: data)
DispatchQueue.main.async {
self.getTokenBalancesModel = apiJsonData.data.items
completion(true)
}
} catch {
print("ERROR:", error)
completion(false)
}
})
}
}

Unable to play mp3 in Swift using URLSession's download data task

I've created a sample blank project with single View Controller in main.storyboard and here is implementation below:
import UIKit
import AVFoundation
private enum DownloadErrors: Error {
case invalidRequest
case noResponse
case noTemporaryURL
case noCacheDirectory
case inplayable
case fileNotExists
case system(error: Error)
var localizedDescription: String {
switch self {
case .invalidRequest:
return "invalid request"
case .noResponse:
return "no response"
case .noTemporaryURL:
return "no temporary URL"
case .noCacheDirectory:
return "no cache directory"
case .inplayable:
return "invalid to play"
case .fileNotExists:
return "file not exists"
case let .system(error):
return error.localizedDescription
}
}
}
class ViewController: UIViewController {
// MARK: - View Life Cycle
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.test()
}
// MARK: - Private API
private func test() {
self.download(with: self.request, completion: { result in
switch result {
case let .failure(error):
print("failure: \(error.localizedDescription)")
return
case let .success(url):
self.play(atURL: url)
}
})
}
private let request: URLRequest? = {
var components = URLComponents()
components.scheme = "https"
components.host = "islex.arnastofnun.is"
components.path = "/islex-files/audio/10/1323741.mp3"
guard let url = components.url else {
return nil
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("audio/mpeg", forHTTPHeaderField: "Content-Type")
request.setValue(
"attachment; filename=\"1323741.mp3\"",
forHTTPHeaderField: "Content-Disposition"
)
print(request.url?.absoluteString ?? "invalid URL")
return request
}()
private typealias Callback = (Result<URL, DownloadErrors>) -> Void
private func download(with nilableRequest: URLRequest?, completion: #escaping Callback) {
guard let request = nilableRequest else {
completion(.failure(.invalidRequest))
return
}
let task = URLSession.shared.downloadTask(with: request) { (rawTemporaryFileURL, rawResponse, rawError) in
if let error = rawError {
completion(.failure(.system(error: error)))
return
}
guard let httpStatusCode = (rawResponse as? HTTPURLResponse)?.statusCode else {
completion(.failure(.noResponse))
return
}
print("http status code: \(httpStatusCode)")
guard let sourceFileURL = rawTemporaryFileURL else {
completion(.failure(.noTemporaryURL))
return
}
guard let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
completion(.failure(.noCacheDirectory))
return
}
let targetFileURL = cache.appendingPathComponent("audio_10_1323741.mp3")
if !FileManager.default.fileExists(atPath: targetFileURL.path) {
do {
try FileManager.default.moveItem(at: sourceFileURL, to: targetFileURL)
} catch let error {
completion(.failure(.system(error: error)))
}
}
DispatchQueue.main.async {
completion(.success(targetFileURL))
}
}
task.resume()
}
private func play(atURL url: URL) {
do {
guard FileManager.default.fileExists(atPath: url.path) else {
print("play: \(DownloadErrors.fileNotExists)")
return
}
let player = try AVAudioPlayer(contentsOf: url)
player.volume = 1.0
player.prepareToPlay()
player.play()
print("play: finished")
} catch let error {
print("play error: \(error.localizedDescription)")
}
}
}
What did I do wrong? I have a success response and get no error while trying to create an audio player with an url, however my player doesn't play anything. I am trying to download and play a file immediately from the link:
Audio
My log in console of Xcode:
https://islex.arnastofnun.is/islex-files/audio/10/1323741.mp3
http status code: 200
play: finished
Since you are inside a completion block, you most likely need to enclose your call inside a DispatchQueue block to explicitly run it on the main thread. In short, you do this:
DispatchQueue.main.async {
self.play(atURL: url)
}