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()
Related
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.
Update 2018-05-25:
I replaced datatask with downloadTask after reading Rob's answer here: https://stackoverflow.com/a/44140059/4666760 . It still does not work when the app is backgrounded.
Hello
I need some help with iOS background tasks. I want to use Apple Push Notification service (APNs) to wake up my app in the background so that it can do a simple RESTful API call to my server. I am able to make it work when the app is in the foreground, but not in the background. I think I do something wrong with the configuration of the URLSession, but I don't know. The entire code for the app and the server is at my repo linked below. Please, clone it and do whatever you like - I just want your help :)
https://github.com/knutvalen/ping
In AppDelegate.swift the app listen for remote notifications:
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
// MARK: - Properties
var window: UIWindow?
// MARK: - Private functions
private func registerForPushNotifications() {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
// MARK: - Delegate functions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
Login.shared.username = "foo"
registerForPushNotifications()
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
os_log("AppDelegate application(_:didRegisterForRemoteNotificationsWithDeviceToken:) token: %#", token)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
os_log("AppDelegate application(_:didFailToRegisterForRemoteNotificationsWithError:) error: %#", error.localizedDescription)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
os_log("AppDelegate application(_:didReceiveRemoteNotification:fetchCompletionHandler:)")
if let aps = userInfo["aps"] as? [String: AnyObject] {
if aps["content-available"] as? Int == 1 {
RestController.shared.onPing = { () in
RestController.shared.onPing = nil
completionHandler(.newData)
os_log("AppDelegate onPing")
}
RestController.shared.pingBackground(login: Login.shared)
// RestController.shared.pingForeground(login: Login.shared)
}
}
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
RestController.shared.backgroundSessionCompletionHandler = completionHandler
}
}
The RestController.swift handles URLSessions with background configurations:
class RestController: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
// MARK: - Properties
static let shared = RestController()
let identifier = "no.qassql.ping.background"
let ip = "http://123.456.7.89:3000"
var backgroundUrlSession: URLSession?
var backgroundSessionCompletionHandler: (() -> Void)?
var onPing: (() -> ())?
// MARK: - Initialization
override init() {
super.init()
let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
configuration.isDiscretionary = false
configuration.sessionSendsLaunchEvents = true
backgroundUrlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
// MARK: - Delegate functions
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let completionHandler = self.backgroundSessionCompletionHandler {
self.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
os_log("RestController urlSession(_:task:didCompleteWithError:) error: %#", error.localizedDescription)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let data = try Data(contentsOf: location)
let respopnse = downloadTask.response
let error = downloadTask.error
self.completionHandler(data: data, response: respopnse, error: error)
} catch {
os_log("RestController urlSession(_:downloadTask:didFinishDownloadingTo:) error: %#", error.localizedDescription)
}
}
// MARK: - Private functions
private func completionHandler(data: Data?, response: URLResponse?, error: Error?) {
guard let data = data else { return }
if let okResponse = OkResponse.deSerialize(data: data) {
if okResponse.message == ("ping_" + Login.shared.username) {
RestController.shared.onPing?()
}
}
}
// MARK: - Public functions
func pingBackground(login: Login) {
guard let url = URL(string: ip + "/ping") else { return }
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 20)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = login.serialize()
if let backgroundUrlSession = backgroundUrlSession {
backgroundUrlSession.downloadTask(with: request).resume()
}
}
func pingForeground(login: Login) {
guard let url = URL(string: ip + "/ping") else { return }
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = login.serialize()
URLSession.shared.dataTask(with: request) { (data, response, error) in
return self.completionHandler(data: data, response: response, error: error)
}.resume()
}
}
By adding App provides Voice over IP services as Required Background Mode in info.plist and using PushKit to handle the APNs payloads I were able to do what I wanted. A SSCCE (example) is available at my repository:
https://github.com/knutvalen/ping
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
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