As title, I'm trying to build a custom url protocol.
I found this and I followed the code provided completely.
However, this delegate
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
is never be triggered or called.
Furthermore, compiler prompted a warning message said that it nearly matches the optionals requirement of URLSessionTaskDelegate, quick fix provided by compiler was to make it private.
So, How do I call the didCompleteWithError delegate, is the code provided in here missing out some parts? Or this is a known issue? Please let me know if there is any workaround solution or a better Swift 3 example of custom UrlProtocol. Thanks in advance!、
Edit 1:
class CustomURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessionTaskDelegate {
private var dataTask: URLSessionDataTask?
private var urlResponse: URLResponse?
private var receivedData: NSMutableData?
class var CustomHeaderSet: String {
return "CustomHeaderSet"
}
// MARK: NSURLProtocol
override class func canInit(with request: URLRequest) -> Bool {
if (URLProtocol.property(forKey: CustomURLProtocol.CustomHeaderSet, in: request as URLRequest) != nil) {
return false
}
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
let mutableRequest = NSMutableURLRequest.init(url: self.request.url!, cachePolicy: NSURLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 240.0)//self.request as! NSMutableURLRequest
//Add User Agent
var userAgentValueString = "myApp"
mutableRequest.setValue(userAgentValueString, forHTTPHeaderField: "User-Agent")
print(mutableRequest.allHTTPHeaderFields ?? "")
URLProtocol.setProperty("true", forKey: CustomURLProtocol.CustomHeaderSet, in: mutableRequest)
let defaultConfigObj = URLSessionConfiguration.default
let defaultSession = URLSession(configuration: defaultConfigObj, delegate: self, delegateQueue: nil)
self.dataTask = defaultSession.dataTask(with: mutableRequest as URLRequest)
self.dataTask!.resume()
print("loaded")
}
override func stopLoading() {
self.dataTask?.cancel()
self.dataTask = nil
self.receivedData = nil
self.urlResponse = nil
}
// MARK: NSURLSessionDataDelegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
self.urlResponse = response
self.receivedData = NSMutableData()
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.client?.urlProtocol(self, didLoad: data as Data)
self.receivedData?.append(data as Data)
}
// MARK: NSURLSessionTaskDelegate
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("completed")
if error != nil { //&& error.code != NSURLErrorCancelled {
self.client?.urlProtocol(self, didFailWithError: error! as! Swift.Error)
} else {
//saveCachedResponse()
self.client?.urlProtocolDidFinishLoading(self)
}
}
}
As the code posted there, those are the changes I made, the 'loaded' was called but 'completed' is never be called.
Edit 2:
Warning message prompted by compiler
Related
I have a WKWebview applying AWS Cognito.
Every request to the server has to be added Authorization into request header.
let access_token = "Bearer \(key)"
let header: [String: String] = [
"Authorization": access_token
]
if let url = URL(string: "https://myserverdomain.amazonaws.com/api/v3/graphs?date=2020-08-28") {
var request: URLRequest = URLRequest(url: url)
request.allHTTPHeaderFields = header
wkWebview.load(request)
}
With this code, I already can load the page content but CSS in the page. I checked with chrome (using ModHeader chrome extension to add header) and it works, show correctly, also Android.
I inspected by Chrome and the CSS link in < head > tag like this, it is not the same folder with the HTML file (I don't know if it is the reason).
<link rel="stylesheet" type="text/css" href="https://myserverdomain.amazonaws.com/assets/graphs/style.css"></script>
I can load the css content only with the code:
let access_token = "Bearer \(key)"
let header: [String: String] = [
"Authorization": access_token
]
if let url = URL(string: "https://myserverdomain.amazonaws.com/assets/graphs/style.css") {
var request: URLRequest = URLRequest(url: url)
request.allHTTPHeaderFields = header
wkWebview.load(request)
}
UIWebview was deprecated, Is there any way to set WKWebview with a global header as always?
Thank you for your help.
You can redirect all webview's requests to your URLSession with your configuration. To do that you can register your custom URLProtocol for https scheme. There is a hack for WKWebView to intercept url requests with WKBrowsingContextController private class and your URLProtocol implementation e.g.:
class MiddlewareURLProtocol : URLProtocol {
static let handledKey = "handled"
lazy var session : URLSession = {
// Config your headers
let configuration = URLSessionConfiguration.default
//configuration.httpAdditionalHeaders = ["Authorization" : "..."]
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
var sessionTask : URLSessionTask?
override var task: URLSessionTask? {
return sessionTask
}
static func registerClass() {
let sel = NSSelectorFromString("registerSchemeForCustomProtocol:")
if let cls = NSClassFromString("WKBrowsingContextController") as? NSObject.Type, cls.responds(to:sel) {
// Register https protocol
cls.perform(sel, with: "https")
}
URLProtocol.registerClass(Self.self)
}
override class func canInit(with request: URLRequest) -> Bool {
return URLProtocol.property(forKey: Self.handledKey, in: request) == nil
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool {
super.requestIsCacheEquivalent(a, to: b)
}
override func startLoading() {
let redirect = (request as NSURLRequest).mutableCopy() as! NSMutableURLRequest
URLProtocol.setProperty(true, forKey: Self.handledKey, in: redirect)
sessionTask = session.dataTask(with: redirect as URLRequest)
task?.resume()
}
override func stopLoading() {
task?.cancel()
}
}
extension MiddlewareURLProtocol : URLSessionDataDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let err = error {
client?.urlProtocol(self, didFailWithError: err)
}
else {
client?.urlProtocolDidFinishLoading(self)
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
client?.urlProtocol(self, didLoad: data)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: #escaping (CachedURLResponse?) -> Void) {
completionHandler(proposedResponse)
}
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
let redirect = (request as NSURLRequest).mutableCopy() as! NSMutableURLRequest
Self.removeProperty(forKey: Self.handledKey, in: redirect)
client?.urlProtocol(self, wasRedirectedTo: redirect as URLRequest, redirectResponse: response)
self.task?.cancel()
let error = NSError(domain: NSCocoaErrorDomain, code: CocoaError.Code.userCancelled.rawValue, userInfo: nil)
client?.urlProtocol(self, didFailWithError: error)
}
}
Just register your protocol on app start to handle all requests:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
MiddlewareURLProtocol.registerClass()
...
}
NOTE: To prevent Apple static checks for private classes you can store class names in the array:
let className = ["Controller", "Context", "Browsing", "WK"].reversed().joined()
I want to get my UIWebView's current location when browsing an SPA (single-page-application) website.
(Yes, I am intentionally working with UIWebView instead of WKWebView.)
I know that there are multiple approaches to getting the current URL, such as:
webView.request?.url?.absoluteString
webView.request?.mainDocumentURL?.absoluteString
webView.stringByEvaluatingJavaScript(from: "window.location")
However, for an SPA website, for each of the approaches above, I am getting the initial url, even when I navigate to another page on that SPA website.
What can I do?
Step 1: Creating a custom URLProtocol to catch data requests
class CustomURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessionTaskDelegate {
// If this returns false, the first request will basically stops at where it begins.
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
print(self.request.url) // URL of Data requests made by UIWebView before loading.
URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil).dataTask(with: self.request).resume()
}
override func stopLoading() {
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
// Data request responses received.
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
// Data request data received.
self.client?.urlProtocol(self, didLoad: data)
self.urlSession(session, task: dataTask, didCompleteWithError: nil)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
self.client?.urlProtocol(self, didFailWithError: error)
return
}
print(self.request.url) // URL of Data requests completed.
self.client?.urlProtocolDidFinishLoading(self)
}
}
Step 2: Registering your custom URLProtocol
Register your custom URLProtocol before loading your SPA in UIWebview.
URLProtocol.registerClass(CustomURLProtocol.self)
Hope this helps! Sorry for the delay, was caught up at work.
I'm going to read file from API, but its size equal 1.6mb and it takes so much time. I wish to read it by parts, and when i founds data which i needs, i'm going to stop recieve data. I trying to use some delegate methods, but they don't works. I don't understand what goes wrong?
I have next code:
class ViewController: UIViewController, URLSessionTaskDelegate, URLSessionDelegate, URLSessionDataDelegate {
var httpString = "hided"
override func viewDidLoad() {
super.viewDidLoad()
getLogBinData()
}
func getLogBinData() {
let session = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue.main)
if let url = URL(string: httpString + "log.bin") {
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = session.dataTask(with: request)
task.resume()
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
print()
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if dataTask.countOfBytesReceived >= 500 {
print(dataTask.countOfBytesReceived)
}
}
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
if dataTask.countOfBytesReceived >= 500 {
print(dataTask.countOfBytesReceived)
}
}
}
according the comment, i edited code and it's worked.
class ViewController: UIViewController, URLSessionTaskDelegate, URLSessionDelegate, URLSessionDataDelegate {
var httpString = "hided"
override func viewDidLoad() {
super.viewDidLoad()
getLogBinData()
}
func getLogBinData() {
let session = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue.main)
if let url = URL(string: httpString + "log.bin") {
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = session.dataTask(with: request)
task.resume()
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if dataTask.countOfBytesReceived >= 500 {
print(dataTask.countOfBytesReceived)
}
}
}
I am trying to add background fetch capability to my app. Currently, the delegate function urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) is called after the network call is complete, but the URL in the cache directory does not exist:
class DownloadManager: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {
static var shared = DownloadManager()
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "my_Identifier")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print(location.absoluteString)
do {
let myData = try Data(contentsOf: location)
} catch let error {
print(error.localizedDescription)
// The The file “file_id.tmp” couldn’t be opened because there is no such file.
}
}
public func fetch() {
guard let url = URL(string: "#{myURL}") else {
return
}
let task = session.downloadTask(with: url)
task.resume()
}
}
And in my App Delegate:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("Executing background fetch")
DownloadManager.shared.fetch()
completionHandler(.newData)
}
What am I missing?
Try using this:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var progressView: UIProgressView!
override func viewDidLoad() {
let _ = DownloadManager.shared.activate()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DownloadManager.shared.onProgress = { (progress) in
OperationQueue.main.addOperation {
self.progressView.progress = progress //assign progress value
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
DownloadManager.shared.onProgress = nil
}
#IBAction func startDownload(_ sender: Any) {
let url = URL(string: "YourFileURL")!
DownloadManager.shared.download(url)
}
}
Replace your DownloadManager:
import Foundation
class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
static var shared = DownloadManager()
var url : URL?
typealias ProgressHandler = (Float) -> ()
var onProgress : ProgressHandler? {
didSet {
if onProgress != nil {
let _ = activate()
}
}
}
override private init() {
super.init()
}
func activate() -> URLSession {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
// Warning: If an URLSession still exists from a previous download, it doesn't create a new URLSession object but returns the existing one with the old delegate object attached!
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
private func calculateProgress(session : URLSession, completionHandler : #escaping (Float) -> ()) {
session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
let progress = downloads.map({ (task) -> Float in
if task.countOfBytesExpectedToReceive > 0 {
return Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive)
} else {
return 0.0
}
})
completionHandler(progress.reduce(0.0, +))
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
if let onProgress = onProgress {
calculateProgress(session: session, completionHandler: onProgress)
}
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
debugPrint("Progress \(downloadTask) \(progress)")
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
debugPrint("Download finished: \(location)")
// try? FileManager.default.removeItem(at: location)
//copy downloaded data to your documents directory with same names as source file
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let destinationUrl = documentsUrl!.appendingPathComponent(url!.lastPathComponent)
let dataFromURL = try? Data(contentsOf: location)
try? dataFromURL?.write(to: destinationUrl, options: [.atomic])
print(destinationUrl)
//now it is time to do what is needed to be done after the download
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
debugPrint("Task completed: \(task), error: \(String(describing: error))")
}
func download(_ url: URL)
{
self.url = url
//download identifier can be customized. I used the "ulr.absoluteString"
let task = DownloadManager.shared.activate().downloadTask(with: url)
task.resume()
}
}
And in my App Delegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
debugPrint("handleEventsForBackgroundURLSession: \(identifier)")
completionHandler()
}
Reference: Tutorial by ralfebert
Hey having trouble with Swift 3. I have the following code:
public var urlSession : URLSession?
self.urlSession = URLSession(configuration: URLSessionConfiguration.default,
delegate: self,
delegateQueue: OperationQueue.main)
I am getting the following error message: "URLSession produces (), not the expected contextual result type URLSession?"
What am I doing wrong here?
My I ask your URLSession functions?
Maybe problem in your code is like:
func URLSession(session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.data.append(data as Data)
}
and :
func URLSession(session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON()
}
}
so instead of:
func URLSession(session:
do this:
func urlSession(_ session:
final will be like:
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.data.append(data as Data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON()
}
}
If its not a solution for your problem please update your question with more code. ✌️
Swift 3.0
let urlString= "URL String"
let myUrl = URL(string: urlString);
let request = NSMutableURLRequest(url:myUrl!);
request.httpMethod = "GET";
let task = URLSession.shared().dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print(error!.localizedDescription)
DispatchQueue.main.sync(execute: {
AWLoader.hide()
})
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSArray
if let parseJSON = json{
print(parseJSON)
} else {
AWLoader.hide()
}
catch{
AWLoader.hide()
print(error)
}
}
task.resume()