Swift and JSON url Image, edited - swift

So this is the code that I am using to create my table view from scratch. My question is how can I parse an image if the image is of string (url) format?
class ArticleCell : UITableViewCell {
var article: Article? {
didSet {
articleTitle.text = article?.title
//articleImage.image = article?.urlToImage
descriptionTitle.text = article?.description
}
}
private let articleTitle : UILabel = {
let lbl = UILabel()
lbl.textColor = .black
lbl.font = UIFont.boldSystemFont(ofSize: 20)
lbl.textAlignment = .left
return lbl
}()
private let descriptionTitle : UILabel = {
let desclbl = UILabel()
desclbl.textColor = .black
desclbl.font = UIFont.boldSystemFont(ofSize: 10)
desclbl.textAlignment = .left
return desclbl
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(articleTitle)
addSubview(descriptionTitle)
Because then what I would like to do is:
addsubview(articleImage)
I get an error as I am declaring an image but it is in a string format. Now, using storyboard is easy, but programmatically I have this issue.
Is it more understandable now? I am so sorry if I made confusion.

To load an image from a URL (or string) we first need to do the following:
guard let urlToImage = article?.urlToImage, let urlContent = URL(string: urlToImage) else { return }
if let data = try? Data(contentsOf: urlContent) {
if let image = UIImage(data: data) {
articleImage.image = image
}
}
}
However this creates a terrible UI experience for the user as image loading from a URL synchronously will hold up our main thread and everyone else in the queue will be stuck waiting for image loading to finish.
For the user's sake, let's set title and description so long but send image loading on some background thread. It should look something like this:
var article: Article? {
didSet {
articleTitle.text = article?.title
descriptionTitle.text = article?.description
guard let urlToImage = article?.urlToImage, let urlContent = URL(string: urlToImage) else { return }
DispatchQueue.global().async {
if let data = try? Data(contentsOf: urlContent) {
if let image = UIImage(data: data) {
DispatchQueue.main.async { [weak self] in
self?.articleImage.image = image
self?.setNeedsLayout()
}
}
}
}
}
}
When we have our image we set it on main thread again and call setNeedsLayout() to tell the cell that it needs to adjust the layout of it's subviews.
NOTE: The above solution works if you are displaying very little cells. If you are displaying MANY cells on your tableview and you want to scroll while they load, you will encounter the age old problem of loading cell content (in this case the image) at the incorrect index path. I suggest reading up on how to asynchronously load images into table and collection views. Have fun!

Related

Convert HTML to NSAttributedString in Background?

I am working on an app that will retrieve posts from WordPress and allow the user to view each post individually, in detail. The WordPress API brings back the post content which is the HTML of the post. (Note: the img tags are referencing the WordPress URL of the uploaded image)
Originally, I was using a WebView & loading the retrieved content directly into it. This worked great; however, the images seemed to be loading on the main thread, as it caused lag & would sometimes freeze the UI until the image had completed downloading. I was suggested to check out the Aztec Editor library from WordPress; however, I could not understand how to use it (could not find much documentation).
My current route is parsing the HTML content and creating a list of dictionaries (keys of type [image or text] and content). Once it is parsed, I build out the post by dynamically adding Labels & Image views (which allows me to download images in background). While this does seem overly-complex & probably the wrong route, it is working well (would be open to any other solutions, though!) My only issue currently is the delay of converting an HTML string to NSAttributedText. Before adding the text content to the dictionary, I will convert it from a String to an NSAttributedString. I have noticed a few seconds delay & the CPU of the simulator getting up to 50-60% for a few seconds, then dropping. Is there anyway I could do this conversion on a background thread(s) and display a loading animation during this time?
Please let me know if you have any ideas or suggestions for a better solution. Thank you very much!
Code:
let postCache = NSCache<NSString, AnyObject>()
var yPos = CGFloat(20)
let screenWidth = UIScreen.main.bounds.width
...
func parsePost() -> [[String:Any]]? {
if let postFromCache = postCache.object(forKey: postToView.id as NSString) as? [[String:Any]] {
return postFromCache
} else {
var content = [[String:Any]]()
do {
let doc: Document = try SwiftSoup.parse(postToView.postContent)
if let elements = try doc.body()?.children() {
for elem in elements {
if(elem.hasText()) {
do {
let html = try elem.html()
if let validHtmlString = html.htmlToAttributedString {
content.append(["text" : validHtmlString])
}
}
} else {
let imageElements = try elem.getElementsByTag("img")
if(imageElements.size() > 0) {
for image in imageElements {
var imageDictionary = [String:Any]()
let width = (image.getAttributes()?.get(key: "width"))!
let height = (image.getAttributes()?.get(key: "height"))!
let ratio = CGFloat(Float(height)!/Float(width)!)
imageDictionary["ratio"] = ratio
imageDictionary["image"] = (image.getAttributes()?.get(key: "src"))!
content.append(imageDictionary)
}
}
}
}
}
} catch {
print("error")
}
if(content.count > 0) {
postCache.setObject(content as AnyObject, forKey: postToView.id as NSString)
}
return content
}
}
func buildUi(content: [[String:Any]]) {
for dict in content {
if let attributedText = dict["text"] as? NSAttributedString {
let labelToAdd = UILabel()
labelToAdd.attributedText = attributedText
labelToAdd.numberOfLines = 0
labelToAdd.frame = CGRect(x:0, y:yPos, width: 200, height: 0)
labelToAdd.sizeToFit()
yPos += labelToAdd.frame.height + 5
self.scrollView.addSubview(labelToAdd)
} else if let imageName = dict["image"] as? String {
let ratio = dict["ratio"] as! CGFloat
let imageToAdd = UIImageView()
let url = URL(string: imageName)
Nuke.loadImage(with: url!, into: imageToAdd)
imageToAdd.frame = CGRect(x:0, y:yPos, width: screenWidth, height: screenWidth*ratio)
yPos += imageToAdd.frame.height + 5
self.scrollView.addSubview(imageToAdd)
}
}
self.scrollView.contentSize = CGSize(width: self.scrollView.contentSize.width, height: yPos)
}
extension String {
var htmlToAttributedString: NSAttributedString? {
guard let data = data(using: .utf8) else { return NSAttributedString() }
do {
return try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding:String.Encoding.utf8.rawValue], documentAttributes: nil)
} catch {
return NSAttributedString()
}
}
var htmlToString: String {
return htmlToAttributedString?.string ?? ""
}
}
( Forgive me for the not-so-clean code! I am just wanting to make sure I can achieve a desirable outcome before I start refactoring. Thanks again! )

How does one change the value of a global variable in a function to be used later on?

I am trying to create a weather application, and in order to do that I need to parse out JSON. This works, (which I know because I tested it by printing), but I cannot change the value of the data I get to an already initialized global variable. A similar question was asked before, but even using the provided answers, I was not able to solve my own problem.
The following lines are where I initialize my global variables:
var currentTemperature = 88
var currentTime = 789
And the following Lines are where I parse the JSON and test by printing in my viewDidLoad function:
let url = URL(string: "https://api.darksky.net/forecast/c269e5928cbdadde5e9d4040a5bd4833/42.1784,-87.9979")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("error")
}
else {
if let content = data {
do {
let json = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
if let jsonCurrently = json["currently"] as? NSDictionary {
if let jsonCurrentTime = jsonCurrently["time"] {
currentTime = jsonCurrentTime as! Int
print(currentTime)
}
if let jsonCurrentTemperature = jsonCurrently["temperature"] {
currentTemperature = jsonCurrentTemperature as! Int
print(currentTemperature)
}
}
} catch {
}
}
}
}
task.resume()
I use the global variables when setting the text of a label in a different class: (however, only the initial value I set to the variable shows up, not the one from the parsed JSON)
let currentTemperatureLabel: UILabel = {
//label of the current temperature
let label = UILabel()
label.text = String(currentTemperature) + "°"
label.textColor = UIColor(red: 150/255, green: 15/255, blue: 15/255, alpha: 0.8)
label.textAlignment = NSTextAlignment.center
label.font = UIFont(name: "Damascus", size: 130)
label.font = UIFont.systemFont(ofSize: 130, weight: UIFontWeightLight)
return label
}()
The JSON example request can be found here: https://darksky.net/dev/docs#api-request-types
No matter what I do, I am not able to use the data from the JSON when I attempt to access the two global variables mentioned before.
var currentTemperature : Double = 88
var currentTime = 789
//...
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://api.darksky.net/forecast/c269e5928cbdadde5e9d4040a5bd4833/42.1784,-87.9979")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("error")
}
else {
if let content = data {
do {
let json = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
if let jsonCurrently = json["currently"] as? NSDictionary {
if let jsonCurrentTime = jsonCurrently["time"] as? Int {
currentTime = jsonCurrentTime
print(currentTime)
}
if let jsonCurrentTemperature = jsonCurrently["temperature"] as? Double {
currentTemperature = jsonCurrentTemperature
print(currentTemperature)
}
}
} catch {
}
}
}
}
task.resume()
}
I did these edits above to your code:
Changed your currentTemperature to be Double (Look at your JSON response to see what kind of response you get and what kind of data type it can be)
When trying to get "time" and "temperature" added optional wrapping to get the data correctly from the response with correct data type so that when assigning to your variables you wont need to do explicit unwrapping
EDIT:
Updated answer based on your comment about the global variables not being part of the class

Apple Vision framework – Text extraction from image

I am using Vision framework for iOS 11 to detect text on image.
The texts are getting detected successfully, but how we can get the detected text?
Recognizing text in an image
VNRecognizeTextRequest works starting from iOS 13.0 and macOS 10.15 and higher.
In Apple Vision you can easily extract text from image using VNRecognizeTextRequest class, allowing you to make an image analysis request that finds and recognizes text in an image.
Here's a SwiftUI solution showing you how to do it (tested in Xcode 13.4, iOS 15.5):
import SwiftUI
import Vision
struct ContentView: View {
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
Image("imageText").scaleEffect(0.5)
SomeText()
}
}
}
The logic is the following:
struct SomeText: UIViewRepresentable {
let label = UITextView(frame: .zero)
func makeUIView(context: Context) -> UITextView {
label.backgroundColor = .clear
label.textColor = .systemYellow
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 25)
return label
}
func updateUIView(_ uiView: UITextView, context: Context) {
let path = Bundle.main.path(forResource: "imageText", ofType: "png")
let url = URL(fileURLWithPath: path!)
let requestHandler = VNImageRequestHandler(url: url, options: [:])
let request = VNRecognizeTextRequest { (request, _) in
guard let obs = request.results as? [VNRecognizedTextObservation]
else { return }
for observation in obs {
let topCan: [VNRecognizedText] = observation.topCandidates(1)
if let recognizedText: VNRecognizedText = topCan.first {
label.text = recognizedText.string
}
}
} // non-realtime asynchronous but accurate text recognition
request.recognitionLevel = VNRequestTextRecognitionLevel.accurate
// nearly realtime but not-accurate text recognition
request.recognitionLevel = VNRequestTextRecognitionLevel.fast
try? requestHandler.perform([request])
}
}
If you wanna know a list of supported languages for recognition, read this post please.
Not exactly a dupe but similar to: Converting a Vision VNTextObservation to a String
You need to either use CoreML or another library to perform OCR (SwiftOCR, etc.)
This will return a overlay image with rectangle box on detected text
Here is the full xcode project
https://github.com/cyruslok/iOS11-Vision-Framework-Demo
Hope it is helpful
// Text Detect
func textDetect(dectect_image:UIImage, display_image_view:UIImageView)->UIImage{
let handler:VNImageRequestHandler = VNImageRequestHandler.init(cgImage: (dectect_image.cgImage)!)
var result_img:UIImage = UIImage.init();
let request:VNDetectTextRectanglesRequest = VNDetectTextRectanglesRequest.init(completionHandler: { (request, error) in
if( (error) != nil){
print("Got Error In Run Text Dectect Request");
}else{
result_img = self.drawRectangleForTextDectect(image: dectect_image,results: request.results as! Array<VNTextObservation>)
}
})
request.reportCharacterBoxes = true
do {
try handler.perform([request])
return result_img;
} catch {
return result_img;
}
}
func drawRectangleForTextDectect(image: UIImage, results:Array<VNTextObservation>) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: image.size)
var t:CGAffineTransform = CGAffineTransform.identity;
t = t.scaledBy( x: image.size.width, y: -image.size.height);
t = t.translatedBy(x: 0, y: -1 );
let img = renderer.image { ctx in
for item in results {
let TextObservation:VNTextObservation = item
ctx.cgContext.setFillColor(UIColor.clear.cgColor)
ctx.cgContext.setStrokeColor(UIColor.green.cgColor)
ctx.cgContext.setLineWidth(1)
ctx.cgContext.addRect(item.boundingBox.applying(t))
ctx.cgContext.drawPath(using: .fillStroke)
for item_2 in TextObservation.characterBoxes!{
let RectangleObservation:VNRectangleObservation = item_2
ctx.cgContext.setFillColor(UIColor.clear.cgColor)
ctx.cgContext.setStrokeColor(UIColor.red.cgColor)
ctx.cgContext.setLineWidth(1)
ctx.cgContext.addRect(RectangleObservation.boundingBox.applying(t))
ctx.cgContext.drawPath(using: .fillStroke)
}
}
}
return img
}

Grand Central Dispatch

I want to speed my code just a little bit. This is my code:
var loadedText : NSAttributedString = NSAttributedString(string: "")
let changeThemeDispatchGroup = DispatchGroup()
DispatchQueue.global(qos: .userInteractive).async {
if self.selectedNote.content != nil
{
changeThemeDispatchGroup.enter()
loadedText = self.selectedNote.content as! NSAttributedString
changeThemeDispatchGroup.leave()
}
else
{
self.noteTextView.becomeFirstResponder()
}
DispatchQueue.main.async
{
self.noteTextView.attributedText = loadedText
}
changeThemeDispatchGroup.notify(queue: DispatchQueue.main)
{
self.changeLetterColor()
}
}
I'm loading loadedText from database and I'm updating the text view. After updating the textview I'm changing the color of each letter. It works great. But now, I want to load loadedText from the database, change the text color and then update the text view. Can you help me out?
Forget the group and change the order
var loadedText = NSAttributedString(string: "")
DispatchQueue.global(qos: .userInteractive).async {
if let content = self.selectedNote.content as? NSAttributedString {
loadedText = content
} else {
self.noteTextView.becomeFirstResponder()
}
DispatchQueue.main.async {
self.changeLetterColor()
self.noteTextView.attributedText = loadedText
}
}

reload tableview without image in cell reloads

I have a tableview and cells within that it has imageview and it gets image from server and whenever user scroll down to end of tableview,tableview's data gets reloaded but my problem is cells image is going to download image from server again whenever tableview gets reloaded, this is my code:
let cell = tableView.dequeueReusableCell(withIdentifier: "pizzacell", for: indexPath) as! CustomPizzaCell
let imgurl = URL(string: "\(BaseUrl)//assests/products/imgs/\(MainMenu.cellarray[indexPath.row].img)")
cell.pizzaimg.kf.setImage(with: imgurl, options: [.downloadPriority(Float(indexPath.row)),.transition(.flipFromTop(1.0)),.forceRefresh,.processor(processor)])
if indexPath.row == MainMenu.cellarray.count-1 {
if !isendoftable {
MainMenu.start += 5
MainMenu.end += 5
var parameters: Parameters = ["START": "\(MainMenu.start)"]
parameters["END"] = "\(MainMenu.end)"
parameters["TYPE"] = "\(MainMenu.type)"
print(parameters)
ApiManager.request(Address: "\(BaseUrl)/admin/Android/getLastProducts", method: .post, parameters: parameters) { json in
self.stopAnimating()
if json == nil {
let popupDialog = PopupDialog(title: "خطای دسترسی به سرور", message: "")
let btn = DefaultButton(title: "تلاش مجدد"){}
btn.titleFont = UIFont(name: "IRANSans(FaNum)", size: 15)
if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad {
btn.titleFont = UIFont(name: "IRANSans(FaNum)", size: 17)
}
popupDialog.addButton(btn)
self.present(popupDialog, animated: true, completion: nil)
}
else {
if (json?.array?.isEmpty)! {
MainMenu.start = 0
MainMenu.end = 5
self.isendoftable = true
}
for item in (json?.array)! {
let piiiza = MainMenu.cell(id: item["id"].description, title: item["title"].description, img: item["img"].description)
MainMenu.cellarray.append(piiiza)
}
tableView.reloadData()
}
}
}
}
return cell
You are using Kingfisher which already handle image caching. Loading a new image when tableView get reloaded should only be reading cache.
You need to remove the .forceRefresh in this line:
cell.pizzaimg.kf.setImage(with: imgurl, options: [.downloadPriority(Float(indexPath.row)),.transition(.flipFromTop(1.0)),.forceRefresh,.processor(processor)])
According to the docs, forceRefreshignore the cache and downloaded from the URL again. You can see this here.
forceRefresh
If set, Kingfisher will ignore the cache and try to fire a download task for the resource.