Using a designated initialiser VS a static method to populate a struct - swift

I have been working on a struct that parses a JSON data, that data is from Dark sky. While I was working on it, something hit my brain and been pondering on it ever since.
Traditionally, we have been using designated initializers (although struct gives you a member-wise initializer for free) to instantiate an object. But, we could easily use a static function that returns itself and that function populates the properties.
like so:
struct WeatherForecastData {
// Weather data
var apparentTemperature: Double?
var icon: String?
var precipProbability: Double?
var pressure: Double?
static func map(_ data: [String: AnyObject]) -> WeatherForecastData {
var p = WeatherForecastData()
p.apparentTemperature = data["apparentTemperature"] as? Double ?? nil
p.icon = data["icon"] as? String ?? nil
p.precipProbability = data["precipProbability"] as? Double ?? nil
p.pressure = data["pressure"] as? Double ?? nil
return p
}
}
notice the static method, if we replace it with a designated initializer, it would be doing the same thing as a static method. My question is when should we use static methods to instantiate an object instead of the traditional designated initializer?

Why wouldn't you just use a custom initializer like this?
struct WeatherForecastData {
var apparentTemperature: Double
var icon: String
var precipProbability: Double
var pressure: Double
init?(data:Dictionary<String,Any>) {
guard
let apparentTemperature = data["apparentTemperature"] as? Double,
let icon = data["icon"] as? String,
let precipProbability = data["precipProbability"] as? Double,
let pressure = data["pressure"] as? Double
else {
print("Invalid Data")
return nil
}
self.apparentTemperature = apparentTemperature
self.icon = icon
self.precipProbability = precipProbability
self.pressure = pressure
}
}

Related

Swift - Encode and Decode a dictionary [String:Any] into plist

I am trying to store the dictionary in my class Marker but it is throwing an error saying it is not encodable or decodable. I can see the error is caused by the [String: Any] but how can I go around it?
var buttonActions : [String: [String: [String:Any]]] = [:]
Save and Load
func saveData() {
let dataFilePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("\(fileName).plist")
let encoder = PropertyListEncoder()
do {
let data = try encoder.encode(markerArray)
try data.write(to: dataFilePath!)
print("Saved")
} catch {
print("Error Encoding \(error)")
}
}
func loadData() {
let dataFilePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("\(fileName).plist")
if let data = try? Data(contentsOf: dataFilePath!){
let decoder = PropertyListDecoder()
do {
markerArray = try decoder.decode([Marker].self, from: data)
} catch {
print("Decode Error \(error)")
}
}
Class
class Marker : Encodable, Decodable {
var UUIDpic: UUID = UUID()
var alpha: Int = 1
var buttonType: Int = 0
var buttonActions : [String: [String: [String:Any]]] = [:]
var buttonNameColor: String = ""
var buttonNameFontSize: Int = 10
var buttonShape: String = ""
var loggerRect: String = ""
var maskColor: String = ""
var name: String = ""
}
Unfortunately you cannot use encode or decode on generic types containing Any (e.g. [String: Any] or [Any]). Any does not conform to protocols Encodable nor Decodable and Swift doesn't know how to encode/decode it. You must use a concrete generic type for your dictionary (e.g. [String: String]).
If you still need to use a general type like Any you have to implement encode(to:) and init(from:) methods. Another option would be to use a struct instead of your [String: [String: [String:Any]]] which conforms to Codable (Encodable & Decodable). You will still have to implement encode(to:) and init(from:) methods in that struct, but the bright side is that you will not have to write the encoder.encode() story for all the properties like you would have to if you implement them in the Marker class.
So finally worked it out with the help of Andrada.
I added a second struct which held the action and by passed having to use [string:any]
class Marker : Encodable, Decodable {
var UUIDpic: UUID = UUID()
var alpha: Int = 1
var buttonType: Int = 0
var buttonAction : [String: [ButtonAction]] = [:] //Dictionary I edited using the new struct
var buttonNameColor: String = ""
var buttonNameFontSize: Int = 10
var buttonShape: String = ""
var loggerRect: String = ""
var maskColor: String = ""
var name: String = ""
}
Below is the struct I added
struct ButtonAction: Codable {
var action: String
var array_linked_of_buttons: [[String:String]]
init(action: String, array_linked_of_buttons: [[String:String]]) {
self.action = action
self.array_linked_of_buttons = array_linked_of_buttons
}
}
Make sure to init your struct or it won't work.

swift 4 model class error: Return from initializer without initializing all stored properties

I created a model class for notes that have the attributes title and content. I am getting an error in the second init that says: Return from initializer without initializing all stored properties. Im feel like there is something I am missing and I cant seem to see it. This is the code:
class Note{
private var _title: String
private var _content: String
var title: String!{
return self._title
}
var content: String!{
return self._content
}
init(title: String, content: String){
self._title = title
self._content = content
}
init(noteData: Dictionary<String, AnyObject>) {
if let title = noteData["title"] as? String {
self._title = title
}
if let content = noteData["content"] as? String {
self._content = content
}
}
}
You have to initialise all properties of your class in its initializer. Regarding your dictionary initializer you should make it fallible and return nil in case of missing key/value pairs:
class Note {
let title: String
let content: String
init(title: String, content: String) {
self.title = title
self.content = content
}
init?(dictionary: [String: Any]) {
guard
let title = dictionary["title"] as? String,
let content = dictionary["content"] as? String
else { return nil }
self.title = title
self.content = content
}
}
Note that if use a struct instead of a class you don't even have to implement an initializer for your custom object:
struct Note {
let title: String
let content: String
init?(dictionary: [String: Any]) {
guard
let title = dictionary["title"] as? String,
let content = dictionary["content"] as? String
else { return nil }
self.title = title
self.content = content
}
}
The problem is that in the second init, if the unwrap fails the properties will not be set. For the init to work correctly the properties that are not optional will all need values. The best way to fix this is to make the properties optional.
Optional properties:
private var title: String?
private var content: String?
By making the properties optional you can avoid doing the if let in the init:
self.title = noteDate["title"]
Make sure to unwrap safely when you use the properties though!

Non-failable initializer requirement cannot be satisfied by failable initializer

I have this struct and I want its init to be failable because something could go wrong with the JSON dictionary I pass:
struct AdminModel:Interface{
var status:Any = ""
var l:String = ""
var p:String = ""
var url:String = ""
init?(json:NSDictionary){
if let status = json["status"] as? Any,
let l = json["l"] as? String,
let p = json["p"] as? String,
let url = json["url"] as? String
{
self.status = status
self.l = l
self.p = p
self.url = url
}else{
return nil
}
}
}
There's no issue until I add ? after init to make init failable: at that point XCode complains:
Non-failable initializer requirement 'init(json:)' cannot be satisfied by failable initializer ('init?')
Why my struct can't be failable? Should I declare failable even the protocol init?
Maybe your Interface is like:
protocol Interface {
init(json: JSON)
}
but your AdminModel's init is init?(json: JSON), so you should keep consistent:
protocol Interface {
init?(json: JSON)
}

Redesign Init (MKAnnotation)

class Point: NSObject, MKAnnotation {
var id: String?
var title: String?
var coordinate: CLLocationCoordinate2D
var subtitle: String?
init(id: String, dictionary: Dictionary<String, AnyObject>){
self.id = id
if let title = dictionary["title"] as? String {
self.title = title
}
if let subtitle = dictionary["subtitle"] as? String {
self.subtitle = subtitle
}
if let coords = dictionary["coordinates"] as? [String:[String:Double]] {
self.coordinate.latitude = coords.values.first!["latitude"]!
self.coordinate.longitude = coords.values.first!["longitude"]!
}
super.init()
}
/*
init(title: String, subtitle: String, coordinate: CLLocationCoordinate2D){
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
}*/
}
I implemented initializer for creating Point on Map. I must redesign init because I use Firebase and results from database are in dictionary.
It writes: Property self.coordinate not initialized at implicitly generated super.init call
init(id: String, dictionary: Dictionary<String, AnyObject>){
self.id = id
if let title = dictionary["title"] as? String {
self.title = title
}
if let subtitle = dictionary["subtitle"] as? String {
self.subtitle = subtitle
}
if let coords = dictionary["coordinates"] as? [String:[String:Double]] {
self.coordinate.latitude = coords.values.first!["latitude"]!
self.coordinate.longitude = coords.values.first!["longitude"]!
}
super.init()
}
So, the problem here mostly lies in the fact that you only ever initialize your coordinate property within an if. Because coordinate is not marked as an optional, it much be given a value before you call super.init(), as per the rules of Swift initializers.
And because you initialize it within an if only, there's a chance that the if fails and you never initialize it at all. This would leave the instance in a completely invalid state, which is not allowed per the rules of Swift initializers and fails the first safety check.
Safety check 1
A designated initializer must ensure that all of the properties
introduced by its class are initialized before it delegates up to a
superclass initializer.
You could make coordinate an optional instance variable, must like your other instance variables. However, I'd argue that probably makes most sense is that id & title are probably also non-optional, and all of these properties probably should be let constants.
As such, I'd rewrite your class and that initializer as such:
class Point: NSObject, MKAnnotation {
let id: String
let title: String?
let coordinate: CLLocationCoordinate2D
let subtitle: String?
init?(id: String, dictionary: [String: AnyObject]){
self.id = id
title = dictionary["title"] as? String
subtitle = dictionary["subtitle"] as? String
guard let
coordinates = dictionary["coordinates"] as? [String: [String: Double]],
latitude = coordinates.values.first?["latitude"],
longitude = coordinates.values.first?["longitude"] else {
return nil
}
coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
super.init()
}
}
Now you have a failable initializer. If any of the values in the dictionary is not correct, the initializer returns nil. Otherwise, your values are all properly initialized.

Swift dynamically cast nil to optional

The following code works:
nil as! String?
"muffin" as! String?
Therefore I also expect this to work:
var magicArray: [Any?] = ["Muffin", nil, 3]
class Box<T> {
var index: Int
init(index: Int){
self.index = index
}
func get() -> T {
return magicArray[index] as! T //crash
}
}
But neither
let box = Box<String?>(index: 0)
box.get()
nor
let box = Box<String?>(index: 1)
box.get()
works as expected. The program crashes at the cast in get(). This however works:
let box = Box<Int>(index: 2)
box.get()
I need to be able to cast a Any? value to T inside my class where T can be any type, including optionals. The Any? actually comes from an array of Any? so there is no other way of verifying it’s of the correct type T.
Is this possible?
If you need a generic wrapper, this should work for you:
final class Wrapper<T> {
final let wrappedValue: T
init(theValue: T) {
wrappedValue = theValue
}
}
You can replace let with var to make it mutable.
Edit:
This is the code I used to test it on a playground.
var s : Any?
s = "some string"
let w = Wrapper(theValue: s)
w.wrappedValue
let int = 3
let w2 = Wrapper(theValue: int)
w2.wrappedValue
w2.wrappedValue = 3
let array : [Any?] = [s, int, nil, "abc", NSObject()]
for obj in array {
let wrapper = Wrapper(theValue: obj)
}