Load html text in WKWebView - html

I use this code to load my html file with text in WKWebView:
do {
guard let filePath = Bundle.main.path(forResource: "\(readBookNumber)", ofType: "html")
else {
print ("File reading error")
return
}
var content = try String(contentsOfFile: filePath, encoding: .utf8)
let baseUrl = URL(fileURLWithPath: filePath)
content.changeHtmlStyle(font: "Iowan-Old-Style", fontSize: UserDefaults.standard.integer(forKey: "textSize"), fontColor: textColor)
webView.loadHTMLString(headerString+content, baseURL: baseUrl)
}
catch {
print ("File HTML error")
}
and this code to load the page where the user stopped reading last time:
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
I use code for loading last page in this method:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}
}
At first I used deadline: .now() + 0.1, but that didn't work. Because the last read page was loaded initially, and after a few seconds I see my text on the first page. I change it to deadline: .now() + 0.5 and the text loads fine from the last page read. Its was 700 pages. But now I want to load another text with 1700 pages. And I have same problem like first time. I can change deadline: .now() + 1.0 and my text will load fine. But I think this is not the best solution. I run it on my iPhone X. But maybe if I run it on iPad mini 2 I should change deadline: .now() + 10.0 because iPad mini 2 not very powerful. How to solve the problem?
Update based on #DPrice code:
If I use this code:
override func viewDidLoad() {
super.viewDidLoad()
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
....
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad\(self.readBookNumber)"))
}
}
}
I have same bad result like in my code.
But if I use this code:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad\(self.readBookNumber)"))
}
}
}
}
Everything works fine. And my last page loading fine. But it does not solve the problem in my question.

Here is a modified version of your ViewController class:
import UIKit
import WebKit
class ViewController: UIViewController, UIScrollViewDelegate, WKNavigationDelegate {
#IBOutlet weak var webView: WKWebView!
#IBOutlet weak var pagesLabel: UILabel!
var readBookNumber = 0
let headerString = "<meta name=\"viewport\" content=\"initial-scale=1.0\" />"
var textSize = 3
var contentSize: CGSize = .zero
override func viewDidLoad() {
super.viewDidLoad()
// Web View Delegate
webView.scrollView.delegate = self
webView.navigationDelegate = self
webView.scrollView.isPagingEnabled = true
webView.scrollView.alwaysBounceVertical = false
webView.scrollView.showsHorizontalScrollIndicator = true
webView.scrollView.showsVerticalScrollIndicator = false
webView.scrollView.panGestureRecognizer.isEnabled = false
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
webView.scrollView.bouncesZoom = false
self.webView.isOpaque = false;
self.webView.backgroundColor = .clear
// Load File
do {
guard let filePath = Bundle.main.path(forResource: "0", ofType: "html")
else {
print ("File reading error")
return
}
var content = try String(contentsOfFile: filePath, encoding: .utf8)
let baseUrl = URL(fileURLWithPath: filePath)
content.changeHtmlStyle(font: "Iowan-Old-Style", fontSize: 4, fontColor: "black")
webView.loadHTMLString(headerString+content, baseURL: baseUrl)
// add content size Observer
webView.scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: .new, context: nil)
}
catch {
print ("File HTML error")
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(UIScrollView.contentSize)) {
let contentSize = webView.scrollView.contentSize
if contentSize != self.contentSize {
self.contentSize = contentSize
DispatchQueue.main.async {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}
}
}
}
// MARK: - webView Scroll View
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.stoppedScrolling()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.stoppedScrolling()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var currentPage = Int((webView.scrollView.contentOffset.x / webView.scrollView.frame.size.width) + 1)
let pageCount = Int(webView.scrollView.contentSize.width / webView.scrollView.frame.size.width)
if currentPage == 0 {
currentPage = 1
} else {
}
if !webView.isHidden {
pagesLabel.text = "\( currentPage ) из \( pageCount )"
} else {
pagesLabel.text = ""
}
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
}
func stoppedScrolling() {
let pageToLoad = Int((webView.scrollView.contentOffset.x))
UserDefaults.standard.set(pageToLoad, forKey: "pageToLoad")
}
// MARK: - loading webView
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Маленькая задержка, которую мне хотелось бы использовать
/*DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}*/
// Большая задержка, которую мне приходится использовать
// don't do this here... we'll do the "auto-scroll" inside the change contentSize Observer
//DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
//}
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
}
}
extension String {
mutating func changeHtmlStyle(font: String, fontSize: Int, fontColor: String) {
let style = "<font face='\(font)' size='\(fontSize)' color= '\(fontColor)'>%#"
self = String(format: style, self)
}
}
It uses an Observer to watch the contentSize change in the web view's scroll view.
Note that it is called multiple times - with different values - during the load and layout process, but it may do the job for you.
Also note, though, that you'll need to account for changes in the web view size - for example, if the user rotates the device. So... more to do, but this may get you going.

You can add a property observer and watch the estimated progress of the page load:
override func viewDidLoad() {
super.viewDidLoad()
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
....
}
and observe when the page is being loaded:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
print ("page loaded")
}
}
}
You may be able to predict based on the page number how far into the loading process you need to be before you set your offset.

Instead of observing WKWebView.estimatedProgress you should observe UIScrollView.contentSize because you need to scroll to an available position e.g.:
var positionY: CGFloat = 1000
var contentSize = CGSize(width: 0, height: 0)
override func viewDidLoad() {
super.viewDidLoad()
...
webView?.scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(UIScrollView.contentSize)) {
if let contentSize = webView?.scrollView.contentSize, contentSize != self.contentSize {
self.contentSize = contentSize
if contentSize.height > positionY {
webView?.scrollView.setContentOffset(CGPoint(x: 0, y: positionY), animated: true)
}
}
}
}

Related

Passing data from StructA to VCA, and then from VCA to VCB

i hope im not breaking any rules, I've got a problem when im trying to pass data from struct to A and from A to B, now the problem happens when im trying to pass the data from B to C. it all works fine when i use delegates from A to B.
before i post my code, i would mention few things:
I parse JSON and use delegate to pass the data from A(is my struct) to B
I would like to send data from VCAA to VCB.
Here's my code:
my struct:
import Foundation
import UIKit
// MARK: - Struct Protocol
protocol QuizBrainDelegate {
func didUpdateQuestionsArray(questionsArr: [Questions])
func didUpdateMessage(message: String)
}
// MARK: -
struct QuizBrain {
let urlString = "https://5fa952f1c9b4e90016e6a5be.mockapi.io/data"
var delegate: QuizBrainDelegate?
// MARK: API Request.
func performRequest() {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, respone, error) in
if error != nil {
print("There's an error \(error!)")
}
if let safeData = data {
self.parseJSON(with: safeData)
}
}
task.resume()
}
}
/// parsing JSON method to parse the JSON
/// - Parameter data: The data returned by the server
func parseJSON(with data: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(Root.self, from: data)
let questionsArr = decodedData.data.questions // an array of questions
let thankUMessage = decodedData.data.thank_you_message
//passing the quiz arr to our Quiz's VC:
self.delegate?.didUpdateQuestionsArray(questionsArr: questionsArr)
self.delegate?.didUpdateMessage(message: thankUMessage)
} catch {
print("There was a problem with parsing JSON \(error)")
}
}
// MARK: - Struct Methods:
/// This func gets the next question everytime we answer the question
/// - Parameters:
/// - questionNum: a counter of the current question number.
/// - numOfQuestions: a counter of total amount of questions.
/// - Returns: returns the new value of questionNum which is the counter of our question number.
func nextQuestion(questionNum: Int, numOfQuestions: Int) -> Int {
return questionNum + 1
}
/// Func sets the score of the player
/// - Parameter scoreNum: the total score number.
/// - Returns: returns the score with 5 points once the user answers right
func getNumOfCurretQuestion(scoreNum: Int) ->Int {
return scoreNum+1
}
}
my VCA:
import UIKit
import Foundation
protocol QuizVCDelegate {
func changeTitle(_ message: String?)
func updateUserOptions(_ optionsArr: [String])
}
class QuizViewController: UIViewController, QuizBrainDelegate {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var answerOption1: UIButton!
#IBOutlet weak var answerOption2: UIButton!
#IBOutlet weak var answerOption3: UIButton!
#IBOutlet weak var answerOption4: UIButton!
#IBOutlet weak var currQuestionLabel: UILabel!
var currentQuestionCounter = 0 // user's current number.
var numOfQuestion = 0 // counter of total questions.
var numOfOptions = 0 // counter of total options for each question
var quizBrain = QuizBrain() // an instance of struct QuizBrain for following MVC.
var messageToDisplay = ""
var quizArr = [Questions]() // array of Q and A
var storedAnswers = [String]() // an array of stored answers of the user
var delegate: QuizVCDelegate?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
quizBrain.performRequest() // Calling the URLRequest.
}
override func viewDidLoad() {
super.viewDidLoad()
quizBrain.delegate = self
}
#IBAction func answerButtonPressed(_ sender: UIButton) {
guard let userAnswer = sender.currentTitle else { return }
storedAnswers.append(userAnswer) // Storing the User's answers.
delegate?.updateUserOptions(storedAnswers)
delegate?.changeTitle(self.messageToDisplay)
sender.pulsate() // lets the user knows that he answered the question.
numOfQuestion = quizBrain.nextQuestion(questionNum: numOfQuestion, numOfQuestions: quizArr.count)
if numOfQuestion == quizArr.count { // checking if its equal to the total of questions in the array.
switchScreen()
}
//to make smooth transitaions im using a timer to update the UI:
if numOfQuestion < quizArr.count {
Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(updateUI), userInfo: nil, repeats: false)
}
}
// a func that updates all the UI
#objc func updateUI() {
questionLabel.fadeTransition(0.4) // fade animation to our questionLabel
self.questionLabel.text = quizArr[numOfQuestion].question
while numOfOptions < quizArr[numOfQuestion].options.count {
switch numOfOptions { // 2-4 Options.
case 0: // none
self.answerOption1.setTitle(self.quizArr[numOfQuestion].options[numOfOptions].value, for: .normal) // Updates option1
case 1: // Joe
self.answerOption2.setTitle(self.quizArr[numOfQuestion].options[numOfOptions].value, for: .normal) // Updates option2
case 2: // Trump
self.answerOption3.setTitle(self.quizArr[numOfQuestion].options[numOfOptions].value, for: .normal) // Updates option3
case 3:
print("There's a case 4")
default:
print("There's a problem with Options Switch Statement")
}
numOfOptions+=1
}
numOfOptions = 0
currQuestionLabel.fadeTransition(0.4) // fade animation to our currentQuestion
currQuestionLabel.text = "Total: \(currentQuestionCounter)/\(numOfQuestion)" // updates the score.
}
/// func to update the arr with the JSON decoded questions and answers.
/// - Parameter questionsArr: an array of question objects.
func didUpdateQuestionsArray(questionsArr: [Questions]) {
DispatchQueue.main.async {
self.quizArr = questionsArr
self.updateUI()
}
}
func didUpdateMessage(message: String) {
DispatchQueue.main.async {
self.messageToDisplay = message
print(self.messageToDisplay)
}
}
// a func which presents our Thank you VC.
func switchScreen() {
let mainStoryboard = UIStoryboard(name: "Main", bundle: Bundle.main)
if let viewController = mainStoryboard.instantiateViewController(withIdentifier: "sbThanks") as? UIViewController {
viewController.modalPresentationStyle = .fullScreen
viewController.modalTransitionStyle = .crossDissolve
self.present(viewController, animated: true, completion: nil)
}
}
}
my VCB:
import Foundation
import UIKit
class ThanksViewController: UIViewController, QuizVCDelegate {
func updateUserOptions(_ optionsArr: [String]) {
DispatchQueue.main.async {
self.choosenAnswers = optionsArr
}
}
func changeTitle(_ message: String?) {
DispatchQueue.main.async {
self.titleLabel.text = message
}
}
var titleLabel = UILabel()
let bodyLabel = UILabel()
var choosenAnswers = [String]()
var quizVC = QuizViewController()
fileprivate func setupLabels() {
titleLabel.lineBreakMode = .byClipping // avoiding the 3 dots.
titleLabel.font = UIFont(name: "Futura", size: 20)
titleLabel.textColor = UIColor.black
titleLabel.textAlignment = .center
bodyLabel.text = "Your Answers:\n\(choosenAnswers)"
bodyLabel.numberOfLines = 0
bodyLabel.textColor = UIColor.black
bodyLabel.textAlignment = .center
}
fileprivate func setupStackView() {
let stackView = UIStackView(arrangedSubviews: [titleLabel, bodyLabel])
stackView.axis = .vertical
stackView.spacing = 8
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -100).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
quizVC.delegate = self
setupLabels()
setupStackView()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapAnimations)))
}
#objc fileprivate func handleTapAnimations() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseOut) {
self.titleLabel.transform = CGAffineTransform(translationX: -30, y: 0)
} completion: { (_) in
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.titleLabel.alpha = 0
self.titleLabel.transform = self.titleLabel.transform.translatedBy(x: 0, y: -100)
})
}
UIView.animate(withDuration: 0.5, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseOut) {
self.bodyLabel.transform = CGAffineTransform(translationX: -30, y: 0)
} completion: { (_) in
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.bodyLabel.alpha = 0
self.bodyLabel.transform = self.bodyLabel.transform.translatedBy(x: 0, y: -100)
})
}
}
}

Open pdf in tableview from a json file

I am in a bind.
My regular developer has secured a full time gig, and I am trying to finish a project.
Literally stuck on the last line of code.....
I have a tableview that parses a json file to populate and open a pdf file when a row is selected.
I reworked the tableview in order to add a searchbar and associated code, which works when running the bundle.
I am, however, stuck with connecting the didSelectRow with the appropriate pdf file.
Can someone please lend a hand? Thanks
import UIKit
import WebKit
// Model
struct Section {
var section: String
var pdfs: [PDF]
}
struct PDF {
var code: String
var name: String
var airport: String
}
class PdfVC: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var webView: WKWebView!
#IBOutlet weak var tableViewLeft: NSLayoutConstraint!
#IBOutlet weak var btnMenuLeft: NSLayoutConstraint!
#IBOutlet weak var btnMenu: UIButton!
#IBOutlet weak var searchBar: UISearchBar!
//json
var jsonData: [Section] = []
var filtedJsonData: [Section] = []
//WebView
var fileName: String?
var isVideo: Bool = false
var btnMenuClose:UIBarButtonItem?
override func viewDidLoad() {
super.viewDidLoad()
hideKeyboardWhenTappedAround()
// Setup TableView
tableView.delegate = self
tableView.dataSource = self
setupData()
tableView.reloadData()
// Setup SearchBar
searchBar.delegate = self
// Setup WebView
if let fileName = fileName {
isVideo = fileName.contains(".mp4")
if let range = fileName.range(of: isVideo ? ".mp4" : ".pdf") {
self.fileName!.removeSubrange(range)
}
}
if let file = Bundle.main.url(forResource: fileName ?? "", withExtension: isVideo ? ".mp4" : ".pdf", subdirectory: nil, localization: nil) {
let request = URLRequest(url: file)
webView.load(request)
}
if let image = UIImage(named: "ico_close") {
navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(onWebTap))
//self.navigationItem.leftItemsSupplementBackButton = true
btnMenuClose = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(onWebTap))
self.navigationItem.rightBarButtonItems = [btnMenuClose!];
}
//navigationItem.title = "Airport Maps"
}
private func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
#objc private func dismissKeyboard() {
view.endEditing(true)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
// title color to white
let titleTextAttributed: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white, .font: UIFont(name: "HelveticaNeue-Light", size: 20) as Any]
self.navigationController?.navigationBar.titleTextAttributes = titleTextAttributed
// back arrow white
self.navigationController?.navigationBar.tintColor = UIColor.white
// blueish navigation bar
self.navigationController?.navigationBar.barTintColor = UIColor(red: 0.00, green: 0.54, blue: 0.92, alpha: 1.00)
self.navigationController?.navigationBar.isTranslucent = false
showMenu(false, animated: false)
}
private func setupData() {
// Parse data from local json file
if let path = Bundle.main.path(forResource: "airports", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
if let jsonResult = jsonResult as? [Dictionary<String, String>] {
for result in jsonResult {
if !jsonData.contains(where: { $0.section == result["Section"] }) {
let section = Section(section: result["Section"] ?? "", pdfs: [])
jsonData.append(section)
}
let pdf = PDF(code: result["Code"] ?? "", name: result["Filename"] ?? "", airport: result["Name"] ?? "")
let index = jsonData.firstIndex(where: { $0.section == result["Section"] })
if let index = index {
jsonData[index].pdfs.append(pdf)
}
}
}
} catch {
// Handle error
print("error parsing json")
}
}
// Sort data before use
jsonData.sort(by: { $0.section < $1.section })
for index in 0..<jsonData.count {
jsonData[index].pdfs.sort(by: { $0.airport < $1.airport } )
}
filtedJsonData = jsonData
}
#objc func onWebTap() {
var left:CGFloat = 0
if tableViewLeft.constant == 0 {
left = -320
}
tableViewLeft.constant = left
showMenu(left != 0, animated: true)
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
func showMenu(_ show:Bool, animated:Bool) {
if let image = UIImage(named: show ? "ico_open" : "ico_close") {
btnMenuClose?.image = image
//navigationItem.rightBarButtonItem?.image = image
//}
//var left:CGFloat = 20
//if show == false {
// left = -64
}
//btnMenuLeft.constant = left
UIView.animate(withDuration: animated ? 0.3 : 0.0, delay: 0, options: .curveEaseInOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
// TableView
extension PdfVC: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return filtedJsonData.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if filtedJsonData[section].pdfs.count == 0 {
return nil
}
return filtedJsonData[section].section
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filtedJsonData[section].pdfs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PdfCell", for: indexPath) as! PdfCell
let pdf = filtedJsonData[indexPath.section].pdfs[indexPath.row]
cell.setupCell(pdf: pdf)
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
dismissKeyboard()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let pdf = filtedJsonData[indexPath.section].pdfs[indexPath.row]
//THIS IS WHERE I'M STUCK<<<<<<<<<
self.title = pdf.airport
}
}
// SearchBar
extension PdfVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
for (index, data) in jsonData.enumerated() {
// Filter pdfs by code and name
let filtedPdf = data.pdfs.filter { $0.code.lowercased().prefix(searchText.count) == searchText.lowercased() || $0.airport.lowercased().prefix(searchText.count) == searchText.lowercased() }
filtedJsonData[index].pdfs = filtedPdf
}
tableView.reloadData()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.text = ""
tableView.reloadData()
}
}
Got it figured out. Thanks for all who spent some time reading my issue. I changed as follows
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let pdf = filtedJsonData[indexPath.section].pdfs[indexPath.row]
self.fileName = pdf.name
self.title = pdf.airport
DispatchQueue.main.async {
self.reloadWebView()
}
}
and added
private func reloadWebView() {
// Setup WebView
if let fileName = fileName {
isVideo = fileName.contains(".mp4")
if let range = fileName.range(of: isVideo ? ".mp4" : ".pdf") {
self.fileName!.removeSubrange(range)
}
}
if let file = Bundle.main.url(forResource: fileName ?? "", withExtension: isVideo ? ".mp4" : ".pdf", subdirectory: nil, localization: nil) {
let request = URLRequest(url: file)
webView.load(request)
}
}

webview executing one of both urls

I'm doing a dictionary application. Some terms have animation, some don't. If ;
let url = URL(string: "http://bsstech.site/-Sozlukler/Fizik/(f.animasyonAdi ?? "").html")!
webview.load(URLRequest(url: url))
or let url = URL(string: "http://bsstech.site/-Sozlukler/Fizik/logo.html")!
webview.load(URLRequest(url: url)) I want to run.
Did I write the code as below, but I did not get the result I wanted.
if let f = fizik {
if (f.animasyonAdi != nil) {
let url = URL(string: "http://bsstech.site/-Sozlukler/Fizik/\(f.animasyonAdi ?? "").html")!
webview.load(URLRequest(url: url))
}else {
let url = URL(string: "http://bsstech.site/-Sozlukler/Fizik/logo.html")!
webview.load(URLRequest(url: url))
}
navigationItem.title = f.baslik
aciklama.text = f.aciklama
}
}
I would be very glad if you help.
Step 1: Create a WebViewViewController
Step 2: Added WebKitView, top title label, a cross button and activity IndicatorView in the WebViewVC.xib file, then insert outlets in the WebViewVC.swift
Step 3: Implement logic in the WebViewVC.swift like the following:
import UIKit
import WebKit
class WebViewVC: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var webView: WKWebView!
#IBOutlet private weak var activityIndicatorView: UIActivityIndicatorView!
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - Variables
private let userAgentValue = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"
var navTitle: String?
var urlString: String?
// MARK: - View Cycle
override func viewDidLoad() {
super.viewDidLoad()
initView()
setupWebView()
loadData()
}
// MARK: - Event
#IBAction private func actionTapToCloseButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
// MARK: - Setting up View Controller
extension WebViewVC {
private func initView() {
titleLabel.text = navTitle
}
private func setupWebView() {
webView.navigationDelegate = self
webView.customUserAgent = userAgentValue
webView.isMultipleTouchEnabled = true
webView.isUserInteractionEnabled = true
}
private func loadData() {
if let `urlString` = urlString, !urlString.isEmpty, let query = urlString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed), let url = URL(string: query) {
let request = URLRequest(url: url)
webView.load(request)
}
}
}
// MARK: - WKNavigationDelegate
extension WebViewVC: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
activityIndicatorView.startAnimating()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
activityIndicatorView.stopAnimating()
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
ShowPopUps.showDefaultAlert(title: "", message: "\(error.localizedDescription)", negativeActionText: "Ok")
activityIndicatorView.stopAnimating()
}
}
Step 4: Just Call
if let f = fizik {
var urlString: String? = nil
if (f.animasyonAdi != nil) {
urlString = http://bsstech.site/-Sozlukler/Fizik/\(f.animasyonAdi ?? "").html"
} else {
urlString = "http://bsstech.site/-Sozlukler/Fizik/logo.html"
}
let vc = WebViewVC()
vc.urlString = urlString
vc.navTitle = f.baslik
present(vc, animated: true, completion: nil)
}

Should I call viewDidLoad() inside updateUIViewController(_:context:) in SwiftUI

I create UIScrollView to be integrated inside SwiftUI view. It contains UIHostingController to host SwiftUI view. When I update UIHostingController, UIScrollView does not change its constraints. I can scroll neither to top nor to bottom. When I try to call viewDidLoad() inside updateUIViewController(_:context:), it works like I expect. Here is my sample code,
struct ContentView: View {
#State private var max = 100
var body: some View {
VStack {
Button("Add") { self.max += 2 }
ScrollableView {
ForEach(0..<self.max, id: \.self) { index in
Text("Hello \(index)")
.frame(width: UIScreen.main.bounds.width, height: 100)
.background(Color(red: Double.random(in: 0...255) / 255, green: Double.random(in: 0...255) / 255, blue: Double.random(in: 0...255) / 255))
}
}
}
}
}
class ScrollViewController<Content: View>: UIViewController, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>! = nil
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scrollView: UIScrollView = UIScrollView()
override func viewDidLoad() {
self.view = UIView()
self.addChild(hostingController)
view.addSubview(scrollView)
scrollView.addSubview(hostingController.view)
scrollView.delegate = self
scrollView.scrollsToTop = true
scrollView.isScrollEnabled = true
makeConstraints()
hostingController.didMove(toParent: self)
}
func makeConstraints() {
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
hostingController.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
}
}
struct ScrollableView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> ScrollViewController<Content> {
let vc = ScrollViewController(rootView: self.content())
return vc
}
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = self.content()
viewController.viewDidLoad()
}
}
I don't think it is a good way to do. I want to know if there is the best way to update controller. If anyone knows the best solution, share me please. Thanks.
You are correct, we should never call our own viewDidLoad.
Let’s diagnose the issue, using the view debugger. So, for example, here it is (setting max to 8 to keep it manageable):
Note the height of the hosting controller’s view is 800 (because we have 8 subviews, 100 pt each). So far, so good.
Now tap the “add” button and repeat:
We can see that the problem isn’t the scroll view, but rather the hosting view controller’s view. Even though there are now 10 items, it still thinks the hosting view controller’s view’s height is 800.
So, we can call setNeedsUpdateConstraints and that fixes the problem:
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = content()
viewController.hostingController.view.setNeedsUpdateConstraints()
}
Thus:
struct ContentView: View {
#State private var max = 8
var body: some View {
GeometryReader { geometry in // don't reference `UIScreen.main.bounds` as that doesn’t work in split screen multitasking
VStack {
Button("Add") { self.max += 2 }
ScrollableView {
ForEach(0..<self.max, id: \.self) { index in
Text("Hello \(index)")
.frame(width: geometry.size.width, height: 100)
.background(Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)))
}
}
}
}
}
}
class ScrollViewController<Content: View>: UIViewController {
var hostingController: UIHostingController<Content>! = nil
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad() // you need to call `super`
// self.view = UIView() // don't set `self.view`
addChild(hostingController)
view.addSubview(scrollView)
scrollView.addSubview(hostingController.view)
// scrollView.delegate = self // you're not currently using this delegate protocol, so we probably shouldn't set the delegate
// scrollView.scrollsToTop = true // these are the default values
// scrollView.isScrollEnabled = true
makeConstraints()
hostingController.didMove(toParent: self)
}
func makeConstraints() {
NSLayoutConstraint.activate([
// constraints for scroll view w/in main view
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// define contentSize of scroll view relative to hosting controller's view
hostingController.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor)
])
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
}
}
struct ScrollableView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> ScrollViewController<Content> {
ScrollViewController(rootView: content())
}
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = content()
viewController.hostingController.view.setNeedsUpdateConstraints()
}
}

how to parse complex JSON and show it inside and outside of CollectionView

I have been dealing with JSON for a month now , getting better day by day. Here is a complex jigsaw I have been circling around. JSON return I get is like this :
{
"main_content": [
{
"product_title": "product 3",
"description": "",
"regular_price": "320",
"sale_price": "",
"product_currency": "$",
"size": [
{
"size_data": "L"
},
{
"size_data": "S"
}
],
"color": [
{
"color_data": "RED"
},
{
"color_data": "WHITE"
}
],
"gallery": [
{
"guid": "http://xxx/wp-content/uploads/2016/11/catagory1.jpg"
},
{
"guid": "http://xxx/wp-content/uploads/2016/11/catagory3.jpg"
}
]
}
]
}
Now , here product_title , description , regular_price , sale_price and product_currency will be outside of array as you can see. For size & color I need to fetch as array outside of my CollectionView , I dunno how to iterate through the array outside of a collectionView or tableView, coz in those I got an indexpath to iterate but I dunno what to do outside a collectionView or tableView. Finally My Image slider will be inside CollectionView therefore gallery items needs to be inside that. Here is visual image of my respective page.
Now here is the POST call using Alamofire in my view
import Foundation
import Alamofire
import SwiftyJSON
//Error could happen for these reason
enum ProductDetailsManagerError: Error {
case network(error: Error)
case apiProvidedError(reason: String)
case authCouldNot(reason: String)
case authLost(reason: String)
case objectSerialization(reason: String)
}
// APIManager Class
class ProductDetailsManager {
// THE RIGHT WAY A.K.A. "THE ONE LINE SINGLETON (NOW WITH PROOF!")
static let sharedInstance = ProductDetailsManager()
func printPublicGists(parameter: [String:Any]? , completionHandler: #escaping (Result<[ProductDetailsJSON]>) -> Void) {
let url: String = "http://xxx/wp-api/products/get_product_informations/"
Alamofire.request(url, method: .post, parameters: parameter, encoding: URLEncoding.default, headers: nil)
.responseJSON { (response) in
guard response.result.error == nil else {
print(response.result.error!)
return
}
guard let value = response.result.value else {
print("no string received in response when swapping oauth code for token")
return
}
print(value)
}
}
func fetchPublicGists(parameter: [String:Any]? , completionHandler: #escaping (Result<[ProductDetailsJSON]>) -> Void) {
let url: String = "http://xxx/wp-api/products/get_product_informations/"
Alamofire.request(url, method: .post, parameters: parameter, encoding: URLEncoding.default, headers: nil)
.responseJSON { response in
let result = self.gistArrayFromResponse(response: response)
completionHandler(result)
}
}
// Download Image from URL
func imageFrom(urlString: String, completionHandler: #escaping (UIImage?, Error?) -> Void) {
let _ = Alamofire.request(urlString)
.response { dataResponse in
// use the generic response serializer that returns Data
guard let data = dataResponse.data else {
completionHandler(nil, dataResponse.error)
return
}
let image = UIImage(data: data)
completionHandler(image, nil)
}
}
//gistArrayFromResponse function
private func gistArrayFromResponse(response: DataResponse<Any>) -> Result<[ProductDetailsJSON]> {
// For Network Error
guard response.result.error == nil else {
print(response.result.error!)
return .failure(RueDu8APIManagerError.network(error: response.result.error!))
}
// JSON Serialization Error, make sure we got JSON and it's an array
guard let jsonArray = response.result.value else {
print("did not get array of homeFeed object as JSON from API")
return .failure(RueDu8APIManagerError.objectSerialization(reason: "Did not get JSON dictionary in response"))
}
//turn JSON into gists
//let gistss = jsonArray.flatMap { HomeFeedJSON(items: $0) }
var gists = [ProductDetailsJSON]()
let jsonR = JSON(jsonArray)
let main_content = jsonR["main_content"].array
for item in main_content! {
gists.append(ProductDetailsJSON(items: item))
}
return .success(gists)
}//gistArrayFromResponse() function ends here
}
here is my model class
import Foundation
import SwiftyJSON
class ProductDetailsJSON {
var _product_title: String?
var _description: String?
var _regular_price: String?
var _sale_price: String?
var _product_currency: String?
var _size: String?
var _color: String?
var _image: URL?
init(items: JSON){
self._product_title = items["product_title"].stringValue
self._description = items["description"].stringValue
self._regular_price = items["regular_price"].stringValue
self._sale_price = items["sale_price"].stringValue
self._product_currency = items["product_currency"].stringValue
let sizeData = items["size"].arrayValue
for itemsIMG in sizeData {
self._size = itemsIMG["size_data"].stringValue
}
let colorData = items["color"].arrayValue
for itemsColor in colorData {
self._size = itemsColor["color_data"].stringValue
}
let galleryImg = items["gallery"].arrayValue
for image in galleryImg {
self._image = image["guid"].URL
}
}
var product_title: String {
if _product_title == nil {
_product_title = ""
}
return _product_title!
}
var description: String {
if _description == nil {
_description = ""
}
return _description!
}
var regular_price: String {
if _regular_price == nil {
_regular_price = ""
}
return _regular_price!
}
var sale_price: String {
if _sale_price == nil {
_sale_price = ""
}
return _sale_price!
}
var product_currency: String {
if _product_currency == nil {
_product_currency = ""
}
return _product_currency!
}
var product_color: String {
if _color == nil {
_color = ""
}
return _size!
}
var product_image: URL {
if _image == nil {
let myURL = "http://www.clker.com/cliparts/d/L/P/X/z/i/no-image-icon-hi.png"
let noImage: URL = URL(string: myURL)!
_image = noImage
}
return _image!
}
}
and here is my controller class where I am struggling to show the size , color and gallery items from JSON
import UIKit
import DropDown
import Alamofire
import SwiftyJSON
class ShopItemVC: UIViewController , UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var contentView: UIView!
#IBOutlet weak var selectedProductImg: UIImageView!
#IBOutlet weak var backgroundCardView1: UIView!
#IBOutlet weak var backgroundCardView2: UIView!
#IBOutlet weak var backgroundCardView3: UIView!
#IBOutlet weak var sizeBtn: NiceButton!
#IBOutlet weak var colorBtn: NiceButton!
#IBOutlet weak var productPrice: UILabel!
#IBOutlet weak var productTitle: UILabel!
// var Title = [ProductDetailsJSON]()
var product_id:Int? //got value from SpecificCatagoryVC
var product_detail = [ProductDetailsJSON]()
var reloadData = 0
let sizeDropDown = DropDown()
let colorDropDown = DropDown()
lazy var dropDowns: [DropDown] = {
return [
self.sizeDropDown,
self.colorDropDown
]
}()
let CatagoryPic = ["catagory1","catagory2","catagory3","catagory4","catagory5","catagory6","c atagory7"]
// let CatagoryPicture = [ProductDetailsJSON]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
sizeBtn.layer.borderWidth = 1.2
sizeBtn.layer.borderColor = UIColor.black.cgColor
colorBtn.layer.borderWidth = 1.2
colorBtn.layer.borderColor = UIColor.black.cgColor
backgroundCardView1.backgroundColor = UIColor.white
backgroundCardView1.layer.cornerRadius = 5.0
backgroundCardView1.layer.masksToBounds = false
backgroundCardView1.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
backgroundCardView1.layer.shadowOffset = CGSize(width: 0, height: 0)
backgroundCardView1.layer.shadowOpacity = 0.8
backgroundCardView2.backgroundColor = UIColor.white
backgroundCardView2.layer.cornerRadius = 5.0
backgroundCardView2.layer.masksToBounds = false
backgroundCardView2.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
backgroundCardView2.layer.shadowOffset = CGSize(width: 0, height: 0)
backgroundCardView2.layer.shadowOpacity = 0.8
backgroundCardView3.backgroundColor = UIColor.white
backgroundCardView3.layer.cornerRadius = 5.0
backgroundCardView3.layer.masksToBounds = false
backgroundCardView3.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
backgroundCardView3.layer.shadowOffset = CGSize(width: 0, height: 0)
backgroundCardView3.layer.shadowOpacity = 0.8
setupDropDowns()
}
override func viewDidAppear(_ animated: Bool) {
self.scrollView.contentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height + 40)
loadGists(parameter: ["product_id":product_id ?? 0])
}
func setupDropDowns() {
setupSizeDropDown()
setupColorDropDown()
}
func setupSizeDropDown() {
sizeDropDown.anchorView = sizeBtn
sizeDropDown.bottomOffset = CGPoint(x: 0, y: sizeBtn.bounds.height)
// You can also use localizationKeysDataSource instead. Check the docs.
sizeDropDown.dataSource = [
"XXL",
"XL",
"L",
"M",
"S"
]
// Action triggered on selection
sizeDropDown.selectionAction = { [unowned self] (index, item) in
self.sizeBtn.setTitle(item, for: .normal)
print(item)
}
}
func setupColorDropDown() {
colorDropDown.anchorView = colorBtn
colorDropDown.bottomOffset = CGPoint(x: 0, y: colorBtn.bounds.height)
// You can also use localizationKeysDataSource instead. Check the docs.
colorDropDown.dataSource = [
"Red",
"Blue",
"White",
"Purple",
"Pink"
]
// Action triggered on selection
colorDropDown.selectionAction = { [unowned self] (index, item) in
self.colorBtn.setTitle(item, for: .normal)
print(item)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func loadGists(parameter: [String:Any]?) {
ProductDetailsManager.sharedInstance.fetchPublicGists(parameter: parameter) {
(result) in
guard result.error == nil else {
self.handleLoadGistsError(result.error!)
return
}
if let fetchedGists = result.value {
self.product_detail = fetchedGists
}
self.reloadData = 1
self.collectionView?.reloadData()
}
}
func handleLoadGistsError(_ error: Error) { }
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return CatagoryPic.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ShopItemCell", for: indexPath) as! ShopItemCell
if reloadData == 1 {
let myProduct = self.product_detail[indexPath.row]
self.productTitle.text = myProduct.product_title
}
cell.shopItemPic.image = UIImage(named: CatagoryPic[(indexPath as NSIndexPath).row])
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// here you know which item is selected by accessing indexPath.item property, for example:
let selectedImage = UIImage(named: CatagoryPic[(indexPath as NSIndexPath).item])
selectedProductImg.image = selectedImage
}
#IBAction func sizeBtnPressed(_ sender: Any) {
sizeDropDown.show()
//print("size btn pressed")
}
#IBAction func colorBtnPressed(_ sender: Any) {
colorDropDown.show()
}
#IBAction func backBtn(_ sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
}
There are some other class where I am facing the same issue. Hope If I get this solution I will be able to solve those. Thanks in advance .
First thing to note is that you are returning the size value and not color in the product_color variable.
Also when you loop through the arrays in your JSON, you're setting the variable to only be the final value. For example here:
let sizeData = items["size"].arrayValue
for itemsIMG in sizeData {
self._size = itemsIMG["size_data"].stringValue
}
The JSON is
"size": [
{
"size_data": "L"
},
{
"size_data": "S"
}
]
So _size is going to be set as "S" and "L" will never be assigned. I'd suggest changing _size, _color and _image to
var _sizes: [String] = []
var _colors: [String] = []
var _images: [String] = []
And then when looping through the JSON array:
let sizeData = items["size"].arrayValue
for itemsIMG in sizeData {
let size = itemsIMG["size_data"].stringValue
_sizes.append(size)
}
If I'm understanding correctly, you then want to update your dropdown data when you get the response with the gist.
sizeDropDown.dataSource = product_detail.product_sizes
Then the same for the other drop downs.
And create product_sizes, product_colors and product_images variables in the model similar sale_price and regular_price.