I am absolutely beginner in Swift and programming at all.
I don't know how to pass a response from my function that calls POST request to the text label on the second view controller. I think that it may be something wrong with the main thread but I don't know how to solve it. I really try to search for the solution but nothing works for me.
Here is my function:
func getBookTitle() -> String {
var bookTitle = ""
if let bookNameURL = URL(string: urlString) {
do {
let htmlString = try String(contentsOf: bookNameURL, encoding: .utf8)
let htmlContent = htmlString
do {
let doc = try SwiftSoup.parse(htmlContent)
do {
let bookNumber = try doc.select("a.dlink").attr("onclick")
let bookNumberTrim = bookNumber.trimmingCharacters(in: CharacterSet.decimalDigits.inverted).components(separatedBy: ",")
do {
let bookid = bookNumberTrim[0]
let passageid = bookNumberTrim[1]
print(bookid)
print(passageid)
//Get Book Title
let url:URL = URL(string: "\(urlString)/getname")!
let session = URLSession.shared
let request = NSMutableURLRequest(url: url)
request.httpMethod = "POST"
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
let paramString = "bookid=\(bookid)&passageid=\(passageid)"
request.httpBody = paramString.data(using: String.Encoding.utf8)
let task = session.dataTask(with: request as URLRequest) { (data, response, error) in
guard let data = data, let _:URLResponse = response, error == nil else {
print("error")
return
}
let dataString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))
do {
let doc: Document = try SwiftSoup.parse(dataString ?? "")
do {
let bTitle = try doc.select("div").first()
bookTitle = try bTitle!.text()
print(bookTitle)
}
} catch {
print(error)
}
}
task.resume()
}
} catch {
print(error)
}
}
} catch let error {
print("Error \(error)")
}
} else {
print("Something wrong")
}
return bookTitle
}
This is how I am trying to pass data to another VC:
@IBAction func infoButtonPressed(_ sender: UIButton) {
performSegue(withIdentifier: "goToInfo", sender: sender)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToInfo" {
let destinationVC = segue.destination as! BookInfoVC
destinationVC.bookName = networkService.getBookTitle()
}
}
And second VC:
class BookInfoVC: UIViewController {
@IBOutlet weak var bookTitle: UILabel!
var bookName: String!
override func viewDidLoad() {
super.viewDidLoad()
bookTitle.text = bookName
}
}
The response that comes from the function is one String line.
Many thanks!
UPD: I've tried to implement completion handler in my function and now it looks like this:
func getBookTitle(completion: @escaping (String) -> ()) {
if let bookNameURL = URL(string: urlString) {
do {
let htmlString = try String(contentsOf: bookNameURL, encoding: .utf8)
let htmlContent = htmlString
do {
let doc = try SwiftSoup.parse(htmlContent)
do {
let bookNumber = try doc.select("a.dlink").attr("onclick")
let bookNumberTrim = bookNumber.trimmingCharacters(in: CharacterSet.decimalDigits.inverted).components(separatedBy: ",")
do {
let bookid = bookNumberTrim[0]
let passageid = bookNumberTrim[1]
//MARK: - Fetch book title
let params = BookTitle(bookid: bookid, passageid: passageid)
AF.request("\(self.urlString)/getname", method: .post, parameters: params).validate(contentType: ["application/x-www-form-urlencoded"]).response { (response) in
if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
do {
let html: String = utf8Text
let doc: Document = try SwiftSoup.parse(html)
do {
let parseTitle = try doc.select("div").first()
let bookTitle = try parseTitle!.text()
completion(bookTitle)
} catch {
print(error)
}
} catch {
print(error)
}
}
}
}
} catch {
print(error)
}
}
} catch let error {
print("Error \(error)")
}
} else {
print("Something wrong")
}
}
And I call it this:
@IBAction func infoButtonPressed(_ sender: UIButton) {
self.performSegue(withIdentifier: "goToInfo", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToInfo" {
let destinationVC = segue.destination as! BookInfoVC
networkService.getBookTitle { bookTitle in
destinationVC.bookName = bookTitle
print(bookTitle)
}
}
I've tried to paste DispatchQueue.main.async everywhere, but it still doesn't update the text label on the second VC. But in the main view controller, it's working fine.
Short answer: You can't. You are making an async request. The function will return before the results are available. You need to rewrite your function to take a completion handler. You would then invoke the completion handler when the result is available. This is widely discussed on this board.
Your updated getBookTitle(completion:)
function looks good, but now you have a sequencing problem.
Your prepare(for segue:)
method in your first view controller uses your networkService.getBookTitle(completion:)
function, and in the completion handler it sets destinationVC.bookName
.
However, remember that networkService.getBookTitle(completion:)
runs asynchronously. It returns immediately, waits for the network result, and then invokes the completion handler.
Your destination view controller, (of type BookInfoVC
) tries to install bookName
into its UI in its viewDidLoad
method. viewDidLoad()
gets called immediately after the first view controller's prepare(for segue:)
fires. At the time viewDidLoad is called, bookName
is very likely still nil.
You need to add a didSet method on your BookInfoVC
's bookName
property so it responds to a change to the book name. That might look like this:
var bookName: String! {
didSet: {
bookTitle.text = bookName
}
}
If you do that, when getBookTitle()'s completion handler fires, it will change destinationVC.bookName
. That will cause the BookInfoVC
's bookName
didSet
code to fire, installing the updated bookName
into the UI.