Swift iOS Cache WKWebView content for offline view - ios

We're trying to save the content (HTML) of WKWebView in a persistent storage (NSUserDefaults, CoreData or disk file). The user can see the same content when he re-enters the application with no internet connection. WKWebView doesn't use NSURLProtocol like UIWebView (see post here).
Although I have seen posts that "The offline application cache is not enabled in WKWebView." (Apple dev forums), I know that a solution exists.
I've learned of two possibilities, but I couldn't make them work:
1) If I open a website in Safari for Mac and select File >> Save As, it will appear the following option in the image below. For Mac apps exists [[[webView mainFrame] dataSource] webArchive], but on UIWebView or WKWebView there is no such API. But if I load a .webarchive file in Xcode on WKWebView (like the one I obtained from Mac Safari), then the content is displayed correctly (html, external images, video previews) if there is no internet connection. The .webarchive file is actually a plist (property list). I tried to use a mac framework that creates a .webarchive file, but it was incomplete.
2) I obtanined the HTML in webView:didFinishNavigation but it doesn't save external images, css, javascript
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()",
completionHandler: { (html: AnyObject?, error: NSError?) in
print(html)
})
}
We're struggling over a week and it is a main feature for us.
Any idea is really appreciated.
Thank you!

I know I'm late, but I have recently been looking for a way to store web pages for offline reading, and still could't find any reliable solution that wouldn't depend on the page itself and wouldn't use the deprecated UIWebView. A lot of people write that one should use the existing HTTP caching, but WebKit seems to do a lot of stuff out-of-process, making it virtually impossible to enforce complete caching (see here or here). However, this question guided me into the right direction. Tinkering with the web archive approach, I found that it's actually quite easy to write your own web archive exporter.
As written in the question, web archives are just plist files, so all it takes is a crawler that extracts the required resources from the HTML page, downloads them all and stores them in a big plist file. This archive file can then later be loaded into the WKWebView via loadFileURL(URL:allowingReadAccessTo:).
I created a demo app that allows archiving from and restoring to a WKWebView using this approach: https://github.com/ernesto-elsaesser/OfflineWebView
EDIT: The archive generation code is now available as standalone Swift package: https://github.com/ernesto-elsaesser/WebArchiver
The implementation only depends on Fuzi for HTML parsing.

I would recommend investigating the feasibility of using App Cache, which is now supported in WKWebView as of iOS 10: https://stackoverflow.com/a/44333359/233602

I'm not sure if you just want to cache the pages that have already been visited or if you have specific requests that you'd like to cache. I'm currently working on the latter. So I'll speak to that. My urls are dynamically generated from an api request. From this response I set requestPaths with the non-image urls and then make a request for each of the urls and cache the response. For the image urls, I used the Kingfisher library to cache the images. I've already set up my shared cache urlCache = URLCache.shared in my AppDelegate. And allotted the memory I need: urlCache = URLCache(memoryCapacity: <setForYourNeeds>, diskCapacity: <setForYourNeeds>, diskPath: "urlCache") Then just call startRequest(:_) for each of the urls in requestPaths. (Can be done in the background if it's not needed right away)
class URLCacheManager {
static let timeout: TimeInterval = 120
static var requestPaths = [String]()
class func startRequest(for url: URL, completionWithErrorCallback: #escaping (_ error: Error?) -> Void) {
let urlRequest = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: timeout)
WebService.sendCachingRequest(for: urlRequest) { (response) in
if let error = response.error {
DDLogError("Error: \(error.localizedDescription) from cache response url: \(String(describing: response.request?.url))")
}
else if let _ = response.data,
let _ = response.response,
let request = response.request,
response.error == nil {
guard let cacheResponse = urlCache.cachedResponse(for: request) else { return }
urlCache.storeCachedResponse(cacheResponse, for: request)
}
}
}
class func startCachingImageURLs(_ urls: [URL]) {
let imageURLs = urls.filter { $0.pathExtension.contains("png") }
let prefetcher = ImagePrefetcher.init(urls: imageURLs, options: nil, progressBlock: nil, completionHandler: { (skipped, failed, completed) in
DDLogError("Skipped resources: \(skipped.count)\nFailed: \(failed.count)\nCompleted: \(completed.count)")
})
prefetcher.start()
}
class func startCachingPageURLs(_ urls: [URL]) {
let pageURLs = urls.filter { !$0.pathExtension.contains("png") }
for url in pageURLs {
DispatchQueue.main.async {
startRequest(for: url, completionWithErrorCallback: { (error) in
if let error = error {
DDLogError("There was an error while caching request: \(url) - \(error.localizedDescription)")
}
})
}
}
}
}
I'm using Alamofire for the network request with a cachingSessionManager configured with the appropriate headers. So in my WebService class I have:
typealias URLResponseHandler = ((DataResponse<Data>) -> Void)
static let cachingSessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = cachingHeader
configuration.urlCache = urlCache
let cachingSessionManager = SessionManager(configuration: configuration)
return cachingSessionManager
}()
private static let cachingHeader: HTTPHeaders = {
var headers = SessionManager.defaultHTTPHeaders
headers["Accept"] = "text/html"
headers["Authorization"] = <token>
return headers
}()
#discardableResult
static func sendCachingRequest(for request: URLRequest, completion: #escaping URLResponseHandler) -> DataRequest {
let completionHandler: (DataResponse<Data>) -> Void = { response in
completion(response)
}
let dataRequest = cachingSessionManager.request(request).responseData(completionHandler: completionHandler)
return dataRequest
}
Then in the webview delegate method I load the cachedResponse. I use a variable handlingCacheRequest to avoid an infinite loop.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let reach = reach {
if !reach.isReachable(), !handlingCacheRequest {
var request = navigationAction.request
guard let url = request.url else {
decisionHandler(.cancel)
return
}
request.cachePolicy = .returnCacheDataDontLoad
guard let cachedResponse = urlCache.cachedResponse(for: request),
let htmlString = String(data: cachedResponse.data, encoding: .utf8),
cacheComplete else {
showNetworkUnavailableAlert()
decisionHandler(.allow)
handlingCacheRequest = false
return
}
modify(htmlString, completedModification: { modifiedHTML in
self.handlingCacheRequest = true
webView.loadHTMLString(modifiedHTML, baseURL: url)
})
decisionHandler(.cancel)
return
}
handlingCacheRequest = false
DDLogInfo("Currently requesting url: \(String(describing: navigationAction.request.url))")
decisionHandler(.allow)
}
Of course you'll want to handle it if there is a loading error as well.
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
DDLogError("Request failed with error \(error.localizedDescription)")
if let reach = reach, !reach.isReachable() {
showNetworkUnavailableAlert()
handlingCacheRequest = true
}
webView.stopLoading()
loadingIndicator.stopAnimating()
}
I hope this helps. The only thing I'm still trying to figure out is the image assets aren't being loaded offline. I'm thinking I'll need to make a separate request for those images and keep a reference to them locally. Just a thought but I'll update this when I have that worked out.
UPDATED with images loading offline with below code
I used the Kanna library to parse my html string from my cached response, find the url embedded in the style= background-image: attribute of the div, used regex to get the url (which is also the key for Kingfisher cached image), fetched the cached image and then modified the css to use the image data (based on this article: https://css-tricks.com/data-uris/), and then loaded the webview with the modified html. (Phew!) It was quite the process and maybe there is an easier way.. but I had not found it. My code is updated to reflect all these changes. Good luck!
func modify(_ html: String, completedModification: #escaping (String) -> Void) {
guard let doc = HTML(html: html, encoding: .utf8) else {
DDLogInfo("Couldn't parse HTML with Kannan")
completedModification(html)
return
}
var imageDiv = doc.at_css("div[class='<your_div_class_name>']")
guard let currentStyle = imageDiv?["style"],
let currentURL = urlMatch(in: currentStyle)?.first else {
DDLogDebug("Failed to find URL in div")
completedModification(html)
return
}
DispatchQueue.main.async {
self.replaceURLWithCachedImageData(inHTML: html, withURL: currentURL, completedCallback: { modifiedHTML in
completedModification(modifiedHTML)
})
}
}
func urlMatch(in text: String) -> [String]? {
do {
let urlPattern = "\\((.*?)\\)"
let regex = try NSRegularExpression(pattern: urlPattern, options: .caseInsensitive)
let nsString = NSString(string: text)
let results = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range) }
}
catch {
DDLogError("Couldn't match urls: \(error.localizedDescription)")
return nil
}
}
func replaceURLWithCachedImageData(inHTML html: String, withURL key: String, completedCallback: #escaping (String) -> Void) {
// Remove parenthesis
let start = key.index(key.startIndex, offsetBy: 1)
let end = key.index(key.endIndex, offsetBy: -1)
let url = key.substring(with: start..<end)
ImageCache.default.retrieveImage(forKey: url, options: nil) { (cachedImage, _) in
guard let cachedImage = cachedImage,
let data = UIImagePNGRepresentation(cachedImage) else {
DDLogInfo("No cached image found")
completedCallback(html)
return
}
let base64String = "data:image/png;base64,\(data.base64EncodedString(options: .endLineWithCarriageReturn))"
let modifiedHTML = html.replacingOccurrences(of: url, with: base64String)
completedCallback(modifiedHTML)
}
}

Easiest way to use cache webpage is as following in Swift 4.0: -
/* Where isCacheLoad = true (Offline load data) &
isCacheLoad = false (Normal load data) */
internal func loadWebPage(fromCache isCacheLoad: Bool = false) {
guard let url = url else { return }
let request = URLRequest(url: url, cachePolicy: (isCacheLoad ? .returnCacheDataElseLoad: .reloadRevalidatingCacheData), timeoutInterval: 50)
//URLRequest(url: url)
DispatchQueue.main.async { [weak self] in
self?.webView.load(request)
}
}

Related

Firebase Storage image download extremely slow

I am developing an IOS app in which I have posts that get fetched from the firestore database. Each post contains references to the firebase storage, where the corresponding images are stored. When I want to download the images, it takes ages for them to be downloaded, around 10-15 seconds. I load them asynchronous. I tried downloading them via
the firebase SDK getData() method
downloading the url and then downloading the content behind the URL
downloading them via passing the url into an asyncImageView
However, none of these methods achieve any good results that could be used for a decent UX. How can I make this faster?
Previous answers suggested making the storage public... Isn't displaying them via the URL a public method?
If it is not and I have to make it public, how can I prevent that everybody can see every image, whether it is a user of the app or not. Is it possible to have a "public" storage but still not making it accessible for everyone?
Should I change to a different provider?
Code:
func orderedImageDownload3(imageRefs: [String], doc: QueryDocumentSnapshot){
let group = DispatchGroup()
var images = [UIImage]()
let storageRef = Storage.storage().reference()
for ref in imageRefs {
let fileRef = storageRef.child(ref)
group.enter()
fileRef.downloadURL { url, error in
if let error = error {
// Handle any errors
print(error)
} else {
//Do the download
if let url = url {
self.getImage(from: url) {data, response, error in
guard let data = data, error == nil else { return }
print(response?.suggestedFilename ?? url.lastPathComponent)
print("Download Finished")
// always update the UI from the main thread
if let image = UIImage(data: data){
images.append(image)
group.leave()
}
}
}
}
}
}
group.notify(queue: .main) {
//put images into observable object
}
}
func getImage(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}

How to publish changes from the background thread Swift UI GET Request

I have set up my app such that I use UserDefaults to store a users login info (isLoggedIn, account settings). If a user is logged in and exits out of the application, and then relaunches the app, I would like them to be returned to the home page tab.
This functionality works; however, for some reason, on relaunch the home page has a getRequest that should be carried out. Instead, the screen goes white. This request and the loading involved works when I navigate from the login, but not when I relaunch the app. I get this warning:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
In looking at other stack overflow posts, the common sentiment seems to be to wrap any type of change in a dispatchqueue.main.async; however, this does not seem to work for me.
import SwiftUI
struct StoresView: View {
#ObservedObject var request = Request()
#Environment(\.imageCache) var cache: ImageCache
#EnvironmentObject var carts: Carts
init() {
getStores()
}
var body: some View {
NavigationView {
List(self.request.stores) { store in
NavigationLink(destination: CategoryHome(store: store).environmentObject(self.carts)) {
VStack(alignment: .leading) {
Text(store.storeName)
.font(.system(size: 20))
}
}
}.navigationBarTitle(Text("Stores").foregroundColor(Color.black))
}
}
func getStores() {
DispatchQueue.main.async {
self.request.getStoresList() { stores, status in
if stores != nil {
self.request.stores = stores!
}
}
}
}
}
get stores call in Request class
class Request: ObservableObject {
#Published var stores = [Store]()
let rest = RestManager()
func getStoresList(completionHandler: #escaping ([Store]?, Int?)-> Void) {
guard let url = URL(string: "###################") else { return }
self.rest.makeRequest(toURL: url, withHttpMethod: .GET, useSessionCookie: false) { (results) in
guard let response = results.response else { return }
if response.httpStatusCode == 200 {
guard let data = results.data else { return}
let decoder = JSONDecoder()
guard let stores = try? decoder.decode([Store].self, from: data) else { return }
completionHandler(stores, response.httpStatusCode)
} else {
completionHandler(nil, response.httpStatusCode)
}
}
}
Make Request from RestManager, I included the make request because I've seen some others use shared dataPublishing tasks, but I may not have used it correctly when trying to use it. Any advice or help would be appreciated. Thanks!
func makeRequest(toURL url: URL,
withHttpMethod httpMethod: HttpMethod, useSessionCookie: Bool?,
completion: #escaping (_ result: Results) -> Void) {
DispatchQueue.main.async { [weak self] in
let targetURL = self?.addURLQueryParameters(toURL: url)
let httpBody = self?.getHttpBody()
// fetches cookies and puts in appropriate header and body attributes
guard let request = self?.prepareRequest(withURL: targetURL, httpBody: httpBody, httpMethod: httpMethod, useSessionCookie: useSessionCookie) else
{
completion(Results(withError: CustomError.failedToCreateRequest))
return
}
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: request) { (data, response, error) in
print(response)
completion(Results(withData: data,
response: Response(fromURLResponse: response),
error: error))
}
task.resume()
}
}
You seem to be trying to call the function in the Main tread instead of setting the stores property. Calling request. getStoresList is already in the main thread once the call is made you enter the background thread from there you need to come back to the main thread once the URLSession is complete. You need to make the UI modification in the Main thread instead of the background tread as the error clearly state. Here's what you need to do to fix this issue:
func getStores() {
self.request.getStoresList() { stores, status in
DispatchQueue.main.async {
if stores != nil {
self.request.stores = stores!
}
}
}
}

Display image from URL, Swift 4.2

I am a fairly decent Objective C developer, and I am now learning Swift (of which I am finding quite difficult, not only because of new concepts, such as optionals, but also because Swift is continually evolving, and much of the available tutorials are severely outdated).
Currently I am trying parse a JSON from a url into an NSDictionary and then use one of its value to display an image (which is also a url). Something like this:
URL -> NSDictionary -> init UIImage from url -> display UIImage in UIImageView
This is quite easy in Objective C (and there may even be a shorter answer):
NSURL *url = [NSURL URLWithString:#"https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"];
NSData *apodData = [NSData dataWithContentsOfURL:url];
NSDictionary *apodDict = [NSJSONSerialization JSONObjectWithData:apodData options:0 error:nil];
The above code snippet gives me back a standard NSDictionary, in which I can refer to the "url" key to get the address of the image I want to display:
"url" : "https://apod.nasa.gov/apod/image/1811/hillpan_apollo15_4000.jpg"
This I then convert into a UIImage and give it to a UIImageView:
NSURL *imageURL = [NSURL URLWithString: [apodDict objectForKey:#"url"]];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage *apodImage = [UIImage imageWithData:imageData];
UIImageView *apodView = [[UIImageView alloc] initWithImage: apodImage];
Now, I am basically trying to replicate the above Objective C code in Swift but continuously run into walls. I have tried several tutorials (one of which actually did the exact same thing: display a NASA image), as well as find a few stack overflow answers but none could help because they are either outdated or they do things differently than what I need.
So, I would like to ask the community to provide the Swift 4 code for the these problems:
1. Convert data from url into a Dictionary
2. Use key:value pair from dict to get url to display an image
If it is not too much already, I would also like to ask for detailed descriptions alongside the code because I would like the answer to be the one comprehensive "tutorial" for this task that I believe is currently not available anywhere.
Thank you!
First of all I'm pretty sure that in half a year you will find Objective-C very complicated and difficult. 😉
Second of all even your ObjC code is discouraged. Don't load data from a remote URL with synchronous Data(contentsOf method. Regardless of the language use an asynchronous way like (NS)URLSession.
And don't use Foundation collection types NSArray and NSDictionary in Swift. Basically don't use NS... classes at all if there is a native Swift counterpart.
In Swift 4 you can easily decode the JSON with the Decodable protocol directly into a (Swift) struct,
the URL string can be even decoded as URL.
Create a struct
struct Item: Decodable {
// let copyright, date, explanation: String
// let hdurl: String
// let mediaType, serviceVersion, title: String
let url: URL
}
Uncomment the lines if you need more than the URL.
And load the data with two data tasks.
let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error { print(error); return }
do {
let decoder = JSONDecoder()
// this line is only needed if all JSON keys are decoded
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Item.self, from: data!)
let imageTask = URLSession.shared.dataTask(with: result.url) { (imageData, _, imageError) in
if let imageError = imageError { print(imageError); return }
DispatchQueue.main.async {
let apodImage = UIImage(data: imageData!)
let apodView = UIImageView(image: apodImage)
// do something with the image view
}
}
imageTask.resume()
} catch { print(error) }
}
task.resume()
You can use this extension
extension UIImage {
public static func loadFrom(url: URL, completion: #escaping (_ image: UIImage?) -> ()) {
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url) {
DispatchQueue.main.async {
completion(UIImage(data: data))
}
} else {
DispatchQueue.main.async {
completion(nil)
}
}
}
}
}
Using
guard let url = URL(string: "http://myImage.com/image.png") else { return }
UIImage.loadFrom(url: url) { image in
self.photo.image = image
}
Since image loading is a trivial and at the same time task which could be implemented in many different ways, I would recommend you to not "reinvent the wheel" and have a look to an image loading library such as Nuke, since it already covers most of the cases you might need during your development process.
It allows you to load and show image asynchronously into your view, using simple api:
Nuke.loadImage(with: url, into: imageView)
And also if you need - to specify how image should be loaded and presented:
let options = ImageLoadingOptions(
placeholder: UIImage(named: "placeholder"),
failureImage: UIImage(named: "failure_image"),
contentModes: .init(
success: .scaleAspectFill,
failure: .center,
placeholder: .center
)
)
Nuke.loadImage(with: url, options: options, into: imageView)
Create an UIIimageView Extension and the following code
extension UIImageView {
public func imageFromServerURL(urlString: String) {
self.image = nil
let urlStringNew = urlString.replacingOccurrences(of: " ", with: "%20")
URLSession.shared.dataTask(with: NSURL(string: urlStringNew)! as URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error as Any)
return
}
DispatchQueue.main.async(execute: { () -> Void in
let image = UIImage(data: data!)
self.image = image
})
}).resume()
}}
and
self.UploadedImageView.imageFromServerURL(urlString: imageURLStirng!)
I have just extended on vadian's answer, separated some concerns to clearly understand the basics. His answer should suffice.
First, you have to build your structure. This will represent the JSON structure you retrieved from the webservice.
struct Item: Codable {
let url, hdurl : URL,
let copyright, explanation, media_type, service_version, title : String
}
Then make you request methods. I usually create a separate file for it. Now, vadian mentioned about completion handlers. These are represented by escaping closures. Here, closure ()-> is passed on both functions and called having the decoded data as argument.
struct RequestCtrl {
func fetchItem(completion: #escaping (Item?)->Void) {
let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")!
//URLSessionDataTask handles the req and returns the data which you will decode based on the Item structure we defined above.
let task = URLSession.shared.dataTask(with: url) { (data, _, _) in
let jsonDecoder = JSONDecoder()
if let data = data,
let item = try? jsonDecoder.decode(Item.self, from: data){
//jsonDecoder requires a type of our structure represented by .self and the data from the request.
completion(item)
} else {
completion(nil)
}
}
task.resume()
}
func fetchItemPhoto(usingURL url: URL, completion: #escaping (Data?)-> Void) {
let task = URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data { completion(data) } else { completion(nil) }
}
task.resume()
}
}
Now in you ViewController, call your request and handle the execution of your closure.
class ViewController: UIViewController {
let requestCtrl = RequestCtrl()
override func viewDidLoad() {
super.viewDidLoad()
requestCtrl.fetchItem { (fetchedItem) in
guard let fetchedItem = fetchedItem else { return }
self.getPhoto(with: fetchedItem)
}
}
func getPhoto(with item: Item) {
requestCtrl.fetchItemPhoto(usingURL: item.url) { (fetchedPhoto) in
guard let fetchedPhoto = fetchedPhoto else { return }
let photo = UIImage(data: fetchedPhoto)
//now you have a photo at your disposal
}
}
}
These are not the best of practices since I am also still learning, so by all means do some research on topics especially closures, ios concurrency and URLComponents on Apple's documentation :)
you need to convert url into string and data to add in imageview
let imageURL:URL=URL(string: YourImageURL)!
let data=NSData(contentsOf: imageURL)
Yourimage.image=UIImage(data: data! as Data)
First add the pod in Podfile
pod 'Alamofire',
pod 'AlamofireImage'
you can check this link for install pods => https://cocoapods.org/pods/AlamofireImage
// Use this function for load image from URL in imageview
imageView.af_setImage(
withURL: url,
placeholderImage: placeholderImage //its optional if you want to add placeholder
)
Check this link for method of alamofireImage
https://github.com/Alamofire/AlamofireImage/blob/master/Documentation/AlamofireImage%203.0%20Migration%20Guide.md
Update for Xcode 13.3 , Swift 5
To load the Image asynchronously from a URL string, use this extension:
extension UIImageView {
public func getImageFromURLString(imageURLString: String) {
guard let imageURL = URL(string: imageURLString) else { return}
Task {
await requestImageFromURL(imageURL)
}
}
private func requestImageFromURL(_ imageURL: URL) async{
let urlRequest = URLRequest(url: imageURL)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
if let httpResponse = response as? HTTPURLResponse{
if httpResponse.statusCode == 200{
print("Fetched image successfully")
}
}
// Loading the image here
self.image = UIImage(data: data)
} catch let error {
print(error)
}
}
}
Usage:
imageView.getImageFromURLString(imageURLString: "https://apod.nasa.gov/apod/image/1811/hillpan_apollo15_4000.jpg")

Swift / UIWebView with xpath / css

I have an element of a website that I want to display within a UIWebView. The element has a unique ID for css as well as for xpath. Is it possible? How would I be able to do that? Help is very appreciated.
I usualy use this code:
let url : NSURL! = NSURL(string: "http://blablablabla.com")
webView.loadRequest(NSURLRequest(URL: url))
But I don't want to display the whole page. Only the element with that ID.
I assume you are using UIWebView, not the newer WKWebView. What you need is an HTML parser (I chose HTMLReader). After downloading the page content, extract the div you want and replace the page's body with the innerHTML of that div.
The code below gets the Did you know section on Wikipedia:
import HTMLReader
override func viewDidLoad() {
super.viewDidLoad()
loadHTML()
}
func loadHTML() {
let url = NSURL(string: "https://en.wikipedia.org/wiki/Main_Page")!
let request = NSURLRequest(URL: url)
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let task = session.dataTaskWithRequest(request) { data, response, error in
guard error == nil else {
print(error!.localizedDescription)
return
}
guard let data = data else {
print("data is nil")
return
}
let html = HTMLDocument(data: data, contentTypeHeader: nil)
if let head = html.firstNodeMatchingSelector("head"),
didYouKnow = html.firstNodeMatchingSelector("#mp-dyk") {
let newHTML = "<html><head>\(head.innerHTML)</head><body>\(didYouKnow.innerHTML)</body></html>"
self.webView.loadHTMLString(newHTML, baseURL: url)
}
}
task.resume()
}
This is rather basic and is not 100% fool-proof though. See if it solves your problem.

How to get Swift to interact with a Webview?

I'm building a basic iOS app with Xcode that mainly just contains a webview with my web app inside.
I was wondering if there was a decent way to save the users username to the devices storage when logging in so that it can be automatically entered when opening the app next time. Since the app is a webview, I don't believe there is a way to keep the user logged in (like other major apps do, such as Facebook), so I think that auto filling the username will be beneficial for them.
I found this question and answer that could possibly solve my problem, although it's in good ol' Objective C.
My current attempt, that does absolutely nothing:
let savedUsername = "testusername"
let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = " + savedUsername + ";"
self.Webview.stringByEvaluatingJavaScriptFromString(loadUsernameJS)
Is this a possibility with Swift?
for storing the password you should use the keychain, specifically web credentials. if done right, this will allow your app to use any existing keychain entries entered via Safari and will also allow Safari to access the password if saved via your app.
Code for setting and retrieving provided below:
private let domain = "www.youdomain.com"
func saveWebCredentials(username: String, password: String, completion: Bool -> Void) {
SecAddSharedWebCredential(domain, username, password) { error in
guard error == nil else { print("error saving credentials: \(error)"); return completion(false) }
completion(true)
}
}
func getExistingWebCredentials(completion: ((String, String)?, error: String?) -> Void) {
SecRequestSharedWebCredential(domain, nil) { credentials, error in
// make sure we got the credentials array back
guard let credentials = credentials else { return completion(nil, error: String(CFErrorCopyDescription(error))) }
// make sure there is at least one credential
let count = CFArrayGetCount(credentials)
guard count > 0 else { return completion(nil, error: "no credentials stored") }
// extract the username and password from the credentials dict
let credentialDict = unsafeBitCast(CFArrayGetValueAtIndex(credentials, 0), CFDictionaryRef.self)
let username = CFDictionaryGetValue(credentialDict, unsafeBitCast(kSecAttrAccount, UnsafePointer.self))
let password = CFDictionaryGetValue(credentialDict, unsafeBitCast(kSecSharedPassword, UnsafePointer.self))
// return via completion block
completion((String(unsafeBitCast(username, CFStringRef.self)), String(unsafeBitCast(password, CFStringRef.self))), error: nil)
}
}
which is used like this:
// save the credentials
saveWebCredentials("hello", password: "world", completion: { success in
// retrieve the credentials
getExistingWebCredentials { credentials, error in
guard let credentials = credentials else { print("Error: \(error)"); return }
print("got username: \(credentials.0) password: \(credentials.1)")
}
})
UPDATE
Recommend switching to using a WKWebView so you can easily pull out the response headers. Here is boilerplate code:
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let webView = WKWebView(frame: self.view.bounds)
webView.navigationDelegate = self
self.view.addSubview(webView)
webView.loadRequest(NSURLRequest(URL: NSURL(string: "https://www.google.com")!))
}
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
// make sure the response is a NSHTTPURLResponse
guard let response = navigationResponse.response as? NSHTTPURLResponse else { return decisionHandler(.Allow) }
// get the response headers
let headers = response.allHeaderFields
print("got headers: \(headers)")
// allow the request to continue
decisionHandler(.Allow);
}
}
You code is not working because you did not wrap savedUsername with quotes.
You should have this instead:
let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = \"\(savedUsername)\";"
Also, this library might help you.
You are not passing a string to JavaScript, you should encapsulate the variable in additional quotes
let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = \"" + savedUsername + "\";"
or
let loadUsernameJS = "document.getElementById('mainLoginUsername').value = '" + savedUsername + "';"

Resources