Optional hell in Swift? - swift

How can I properly handle optionals especially when it is multi-level optional?
For example,
let html = String(data: response.data!, encoding: .utf8)
if let string = html {
let doc = try? SwiftSoup.parse(html)
let links = try? doc?.select("a").array().map{try? $0.attr("href")}
}
// links!![0]! is crazy
Constant links is like below: optional of optional array of optional strings
Optional(Optional([Optional("abc.com"), Optional("def.com")]))
Is there a pattern which is more proper than do-try-catch block or optional binding?

Replace most of the try? with try in a do/catch. You only need one do/catch with your multiple try. Use if let more. Use flatMap instead of map.
if let html = String(data: response.data!, encoding: .utf8) {
do {
let doc = try SwiftSoup.parse(html)
let links = try doc.select("a").array().flatMap { try? $0.attr("href") }
} catch {
// Uh-oh
}
}
I can't test this so this may not be perfect but it should get you most of the way there.

You can chain several if let statements in a single command, like this:
if let string = html, let doc = try? SwiftSoup.parse(html) {
...
}

You've got several choices, none of which are really more "proper" than the others, so it's a matter of taste. But I'd probably do one of:
A compound if statement:
if let responseData = response.data,
let string = String(data: responseData, encoding: .utf8),
let doc = try? SwiftSoup.parse(html),
let links = try? doc.select("a").array().compactMap({ try? $0.attr("href") }) {
// do something with links, any nil will get filtered out from the array
// (if using earlier than Swift 4.1, use flatMap instead of compactMap)
}
guard statements:
guard let responseData = response.data else { /* bail somehow */ }
guard let string = String(data: responseData, encoding: .utf8) else { /* bail somehow */ }
... etc ...
Alternately, one long guard statement written like the if statement above.
Or, y'know, just use a good old-fashioned do/try/catch block.

Related

Initiating string with content of file (Swift)

Swift newbie here. I am trying load a text file into a string using the following code:
var uncondString: String
if let tempstring = try? String(contentsOf: url, encoding: .utf8) {
uncondString = tempstring
}
print("\(uncondString)")
The print statement, however, throws error "Variable 'uncondString' used before being initialized"
I guess this is trivial for more experienced users but any help is appreciated.
If the statement try? fails, it returns nil, and the if let statement is not executed when nil is returned
After all, this code is not a code that unconditionally succeeds in initialization, so a warning is displayed.
var uncondString: String
if let tempstring = try? String(contentsOf: url, encoding: .utf8) {
uncondString = tempstring
print(uncondString)
}
or
let uncondString: String? = try? String(contentsOf: url, encoding: .utf8)
if let uncondStr = uncondString {
print(uncondStr)
}

Swift 4: Type of expression is ambiguous without more context inside loop

I have a question:
I'm retrieving a long string made of some base 64 strings attached together with ";" separating each of them inside said string.
Here's my code:
if(item.photo != "null"){
let b64fullstring = item.photo
if(b64fullstring!.contains(";")){
let photos = b64fullstring!.split(separator: ";")
for pic in photos{
let base64encodedstring = pic
let decodedData = Data(base64Encoded: base64encodedstring!, options: Data.Base64DecodingOptions.ignoreUnknownCharacters)!
let decodedString = String(data: decodedData, encoding: .utf8)!
print(pic)
}
}
}
Its gives me the following error on the "data" function;
Type of expression is ambiguous without more context
I really don't get it.
When working on a single string, it works perfectly fine. But when using a loop, it gives this message for some reason.
Thank you for taking some of your time for helping me.
Swift errors are not very helpful. The problem there is that split method returns an array of substrings:
func split(separator: Character, maxSplits: Int = Int.max, omittingEmptySubsequences: Bool = true) -> [Substring]
And the Data initializer expects a String:
init?(base64Encoded base64String: String, options: Data.Base64DecodingOptions = [])
You just need to initialize a new string from your substring:
if let photos = b64fullstring?.split(separator: ";") {
for pic in photos {
if let decodedData = Data(base64Encoded: String(pic), options: .ignoreUnknownCharacters) {
if let decodedString = String(data: decodedData, encoding: .utf8) {
print(pic)
}
}
}
}
Another option is to use components(separatedBy:) method which returns an array of strings instead of substrings:
func components<T>(separatedBy separator: T) -> [String] where T : StringProtocol
if let photos = b64fullstring?.components(separatedBy: ";") {
for pic in photos {
if let decodedData = Data(base64Encoded: pic, options: .ignoreUnknownCharacters) {
if let decodedString = String(data: decodedData, encoding: .utf8) {
print(pic)
}
}
}
}

How can I convert a string, such as "iso-8859-1", to it's String.Encoding counterpart?

After sending a HTTP request from Swift, I get a field in the response called textEncodingName.
I want to convert the data object I also received into a string containing its contents, and to do this, I'm using String(data: data!, encoding: .utf8). This works most of the time, because most websites are UTF-8 encoded. But with, for example, https://www.google.co.uk, the response.textEncodingName == "iso-8859-1".
I guess other websites would use even more obscure encodings, so my question is this: how can I find the right encoding to convert my data object to the correct string.
You can simply try String.Encoding.windowsCP1250 for iso-8859-1. Please refer https://en.wikipedia.org/wiki/Windows-1250
String(data: data, encoding: .windowsCP1250)
OR..
I found a few steps that will take you from the textEncodingName to the corresponding String.Encoding value:
let estr = "iso-8859-1"
let cfe = CFStringConvertIANACharSetNameToEncoding(estr as CFString)
let se = CFStringConvertEncodingToNSStringEncoding(cfe)
let encoding = String.Encoding(rawValue: se)
This is largely based on the documentation for URLResponse.textEncodingName:
You can convert this string to a CFStringEncoding value by calling CFStringConvertIANACharSetNameToEncoding(:). You can subsequently convert that value to an NSStringEncoding value by calling CFStringConvertEncodingToNSStringEncoding(:).
Here's an update that checks to see if the original text encoding string is valid or not:
let estr = "XXX"
let cfe = CFStringConvertIANACharSetNameToEncoding(estr as CFString)
if cfe != kCFStringEncodingInvalidId {
let se = CFStringConvertEncodingToNSStringEncoding(cfe)
let sse = String.Encoding(rawValue: se)
print("sse = \(sse)")
} else {
print("Invalid")
}
I would write an enum with a String raw value and a computed property to return the appropriate String.Encoding value. Then you can use its init(rawValue:) to create an instance.
import Foundation
enum APITextEncoding : String
{
case iso8859_1 = "iso-8859-1"
// etc.
var encoding: String.Encoding
{
switch self
{
case .iso8859_1:
return .isoLatin1
// etc.
}
}
}
let receivedEncoding = APITextEncoding(rawValue: encodingDescription)
let receivedText = String(data: receivedData, encoding: receivedEncoding.encoding)
In Swift You can use:
guard let string = String(data: data, encoding: .isoLatin1) else {return}
guard let perfectData = string.data(using: .utf8, allowLossyConversion: true) else {return}
In swift you can use:
func getTextFrom(_ url: URL) -> String? {
guard let data = try? Data(contentsOf: url) else {
return nil
}
return String(data: data, encoding: .utf8) ??
String(data: data, encoding: .isoLatin1)
}

Expected else after guard statement, but else is there, why is it complaining?

I am trying to do PropertyListSerialization as opposed to using just NSDictionary(contentsOfFile:) so I changed my code for that. Now using guard, I have to give else. I am providing else. What mistake am I making in this line of code?
It says:
Expected 'else' after 'guard' condition.
This line of code when using NSDictionary(contentsOfFile:) was working fine. But this change to PropertyListSerialization is not working for me.
guard
let filePathValue = filePath,
// let fileContentDictionary:NSDictionary = NSDictionary(contentsOfFile: filePathValue)
let data = try? Data(contentsOf: filePathValue as URL),
let result = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [[String:Any]]
print(result!)
else{
return
}
You can access the result after that else block also if your response is dictionary then you need to cast the result of propertyList(from:options:format:) to [String:Any] not the [[String:Any]] because it is dictionary.
guard let filePathValue = filePath,
//let fileContentDictionary: NSDictionary = NSDictionary(contentsOfFile: filePathValue)
let data = try? Data(contentsOf: filePathValue as URL),
let result = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String:Any] //not [[String:Any]]
else {
return
}
//Result is still optional
print(result!)
Now as of you have used try? your result is still optional so if you want result as non-optional then use () with try? like this.
guard let filePathValue = filePath,
//let fileContentDictionary: NSDictionary = NSDictionary(contentsOfFile: filePathValue)
let data = try? Data(contentsOf: filePathValue as URL),
let result = (try? PropertyListSerialization.propertyList(from: data, options: [], format: nil)) as? [String:Any] //not [[String:Any]]
else {
return
}
//Now result is non-optional
print(result)
Your print(result!) is a problem.
Remove it.
Update:
guard let filePathValue = filePath,
//let fileContentDictionary: NSDictionary = NSDictionary(contentsOfFile: filePathValue)
let data = try? Data(contentsOf: filePathValue as URL),
let result = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [[String:Any]]
else {
return
}
print(result!)
The basic guard-else block looks like this:
guard `condition` else {
`statements`
}
Example:
guard let name = optionalName else {
// Value requirements not met, do something.
// name isn't available here
return
}
// Value requirements met, do something with name
// name is available here
// Rest of the code goes here
You have to consider guard-else as a single program control statement. No statements are made in between them. Only conditions are checked. You may have multiple conditions separated by commas (,).
From the documentation it says:
Any constants or variables assigned a value from an optional binding
declaration in a guard statement condition can be used for the rest of
the guard statement’s enclosing scope.

How To Update A Label In Swift 3 With JSON Data Inside Of A Function?

For some reason whenever I try to update my label with the current temperature using self.infoLabel.text = String(temp!) inside of the DispatchQueue code block, I get the following fatal error message:
unexpectedly found nil while unwrapping an Optional value.
I'd appreciate if someone could help me figure out why the code below isn't working. Thanks.
func getCurrentTemp(city: String){
let weatherRequestURL = URL(string: "\(openWeatherMapBaseURL)?APPID=\(openWeatherMapAPIKey)&q=\(city)")!
// The data task retrieves the data.
URLSession.shared.dataTask(with: weatherRequestURL) { (data, response, error) in
if let error = error {
// Case 1: Error
print("Error:\n\(error)")
}
else {
//print("Raw data:\n\(data!)\n")
//let dataString = String(data: data!, encoding: String.Encoding.utf8)
//print("Human-readable data:\n\(dataString!)")
do {
// Try to convert that data into a Swift dictionary
let weather = try JSONSerialization.jsonObject(with: data!, options:.allowFragments) as! [String:AnyObject]
if let main = weather["main"] as? [String: Any] {
let temp = main["temp"] as? Double
print("temp\(temp!)")
DispatchQueue.main.sync(execute: {
self.infoLabel.text = String(temp!)
})
//return temp as? String
//let temp_max = main["temp_max"] as? Double
//print("temp\(temp_max!)")
//let temp_min = main["temp_min"] as? Double
//print("temp\(temp_min!)")
}
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
.resume()
}
There are two possibilities here: 1) either temp is nil (and it shouldn't be because you already force unwrap it in the print statement above) 2) or infoLabel is nil which happens if you broke your outlet connection.
Its easy to check; make a breakpoint above your assignment and in the debug console you can type:
po self.infoLabel
to see if its nil. For good measure you an also check temp.
You can also add a print statement to check self.infoLabel or an assert.
Alright, so I found a makeshift solution to this issue (See Below). Rather than placing the code inside of the function I made, I placed it in the viewDidLoad() function. For whatever reason, self.infoLabel? would be nil anywhere inside of the function I made.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
print("Sucessful launched weather page.")
let weatherRequestURL = URL(string: "\(openWeatherMapBaseURL)?APPID=\(openWeatherMapAPIKey)&q=\(city)")!
// The data task retrieves the data.
URLSession.shared.dataTask(with: weatherRequestURL) { (data, response, error) in
if let error = error {
// Case 1: Error
print("Error:\n\(error)")
}
else {
//print("Raw data:\n\(data!)\n")
//let dataString = String(data: data!, encoding: String.Encoding.utf8)
//print("Human-readable data:\n\(dataString!)")
do {
// Try to convert that data into a Swift dictionary
let weather = try JSONSerialization.jsonObject(with: data!, options:.allowFragments) as! [String:AnyObject]
if let main = weather["main"] as? [String: Any] {
let temp = main["temp"] as? Double
print("temp\(temp!)")
var tempInFarenheit = ((9/5)*((temp!)-273) + 32).rounded()
DispatchQueue.main.sync(execute: {
self.infoLabel.text = "\(tempInFarenheit) + °"
})
}
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
.resume()
}
Although this isn't the most effective way of doing things, hopefully it can help others who are having the same problem. If I find a more effective way of doing this, I'll be sure to edit this post and include it.