Swift: The proper way to initialize model class with a lot of properties - swift

How do you initialize your classes/structs with a lot of properties?
This question could probably be asked without Swift context but Swift brings a flavour to it, so I add Swift tag in headline and tags.
Let's say you have a User class with 20 properties. Most of them should not be nil or empty. Let's assume these properties are not interdependent. Let's assume that 33% of it should be constant (let) by the logic of the class. Let's assume that at least 65% of them do not have meaningful default values. How would you design this class and initialize an instance of it?
So far I have few thoughts but none of it seems to be completely satisfactory to me:
put all of the properties linearly in the class and make huge init method:
class User {
// there is 20 properties like that
let id : String
let username : String
let email : String
...
var lastLoginDate : Date
var lastPlayDate : Date
// then HUUUUGE init
init(id: String,
username: String,
...
lastPlayDate: Date) {
}
}
try to group properties into sub types and deal with smaller inits separately
class User {
struct ID {
let id : String
let username : String
let email : String
}
struct Activity {
var lastLoginDate : Date
var lastPlayDate : Date
}
let id : ID
...
var lastActivity : Activity
// then not so huge init
init(id: ID,
...
lastActivity: Activity) {
}
}
another solution is to break requirements a bit: either declare some of the properties optional and set values after init or declare dummy default values and set normal values after init, which conceptually seems to be the same
class User {
// there is 20 properties like that
let id : String
let username : String
let email : String
...
var lastLoginDate : Date?
var lastPlayDate : Date?
// then not so huge init
init(id: String,
username: String,
email: String) {
}
}
// In other code
var user = User(id: "1", username: "user", email: "user#example.com"
user.lastLoginDate = Date()
Is there a nice paradigm/pattern how to deal with such situations?

You can try the builder pattern.
Example
class DeathStarBuilder {
var x: Double?
var y: Double?
var z: Double?
typealias BuilderClosure = (DeathStarBuilder) -> ()
init(buildClosure: BuilderClosure) {
buildClosure(self)
}
}
struct DeathStar : CustomStringConvertible {
let x: Double
let y: Double
let z: Double
init?(builder: DeathStarBuilder) {
if let x = builder.x, let y = builder.y, let z = builder.z {
self.x = x
self.y = y
self.z = z
} else {
return nil
}
}
var description:String {
return "Death Star at (x:\(x) y:\(y) z:\(z))"
}
}
let empire = DeathStarBuilder { builder in
builder.x = 0.1
builder.y = 0.2
builder.z = 0.3
}
let deathStar = DeathStar(builder:empire)
Example taken from here: https://github.com/ochococo/Design-Patterns-In-Swift
If you are looking for a bit more Java like solution, you can try something like this.
Alternative Example
final class NutritionFacts {
private let servingSize: Int
private let servings: Int
private let calories: Int
private let fat: Int
private let sodium: Int
private let carbs: Int
init(builder: Builder) {
servingSize = builder.servingSize
servings = builder.servings
calories = builder.calories
fat = builder.fat
sodium = builder.sodium
carbs = builder.carbs
}
class Builder {
let servingSize: Int
let servings: Int
private(set) var calories = 0
private(set) var fat = 0
private(set) var carbs = 0
private(set) var sodium = 0
init(servingSize: Int, servings: Int) {
self.servingSize = servingSize
self.servings = servings
}
func calories(value: Int) -> Builder {
calories = value
return self
}
func fat(value: Int) -> Builder {
fat = value
return self
}
func carbs(value: Int) -> Builder {
carbs = value
return self
}
func sodium(value: Int) -> Builder {
sodium = value
return self
}
func build() -> NutritionFacts {
return NutritionFacts(builder: self)
}
}
}
let facts = NutritionFacts.Builder(servingSize: 10, servings: 1)
.calories(value: 20)
.carbs(value: 2)
.fat(value: 5)
.build()
Example taken from: http://ctarda.com/2017/09/elegant-swift-default-parameters-vs-the-builder-pattern

Related

Accept Float, Double or Int in Swift `init` to convert to String

I'm attempting to convert a value to a string in an init. That value can be an Int, Double, or Float.
For example:
struct example {
var string: String
init(number: Int) {
string = String(number)
}
}
I would like to say something like below (I'm using Typescript as an example - this obviously doesn't work, but I want number to be any of those three types - all of which String can convert).
struct example {
var string: String
init(number: Int | Float | Double) {
string = String(number)
}
}
Edit: I realized I have another issue. I need to convert back from a string to the type of Int or Double or Float. Using one of the answers below, I'm trying to figure out how to implement getNumberWith5:
struct example2<N: Numeric & CustomStringConvertible>: CustomStringConvertible {
#Binding var number: N
init(number: Binding<N>) {
self._number = number
}
var description: String {
String(describing: number)
}
mutating func getNumberWith5() {
// How do I update number to the type N?
self.number = howDoIConvertToN(description + "5")
}
}
Or from another answer:
struct example3<N: Numeric> {
#Binding var number: N
var string: String
init(number: Binding<N>) {
self._number = number
self.string = "\(number)"
}
mutating func getNumberWith5() {
// How do I update number to the type N?
self.number = howDoIConvertToN(string + "5")
}
}
Edit2 My Answer:
I attempted to create an equivalent of type unions (as Typescript has) using enums in Swift based on this article. But it was challenging to then assign back to that value. I've decided Swift just doesn't have first class support for type unions like Typescript has. So, I used the accepted answer below and this seems to work.
extension String {
func numeric<N: Numeric & LosslessStringConvertible>() -> N? {
N(self)
}
}
struct example4<N: Numeric & LosslessStringConvertible> {
#State var string: String
#Binding var number: N
init(number: Binding<N>) {
self._number = number
self.string = String(describing: number)
}
mutating func getNumberWith5() {
let newString = string + "5"
number = newString.numeric() ?? 0
}
}
Actually if all you want is a string representation of Int Float Double or any other standard numeric type you only need to know that they conform to CustomStringConvertible and use String(describing:).
Or you can use conformance to Numeric and CustomStringConvertible:
struct example {
var string: String
init<C: CustomStringConvertible & Numeric>(number: C) {
string = String(describing: number)
}
}
and maybe even better example itself could conform to CustomStringConvertible
struct example: CustomStringConvertible {
var description: String
init<C: CustomStringConvertible & Numeric>(number: C) {
description = String(describing: number)
}
}
yet another way :
struct example<N: Numeric & CustomStringConvertible>: CustomStringConvertible {
let number: N
init(number: N) {
self.number = number
}
var description: String {
String(describing: number)
}
}
EDIT
I think what you want is a custom Property Wrapper not #Binding:
#propertyWrapper struct CustomStringConversion<Wrapped: CustomStringConvertible> {
var wrappedValue: Wrapped
init(wrappedValue: Wrapped) {
self.wrappedValue = wrappedValue
}
var projectedValue: String { .init(describing: wrappedValue) }
}
struct Foo {
#CustomStringConversion var number = 5
}
let foo = Foo()
let number: Int = foo.number // 5
let stringRepresentation: String = foo.$number // "5"
But as #LeoDabus pointed out using LosslessStringConvertible may be better :
struct example<N: Numeric & LosslessStringConvertible>: LosslessStringConvertible {
let number: N
init(number: N) {
self.number = number
}
init?(_ description: String) {
guard let number = N(description) else { return nil }
self.number = number
}
var description: String {
.init(number)
}
}
let bar = example(number: Double.greatestFiniteMagnitude) // 1.7976931348623157e+308
let baz: example<Double>? = example("1.7976931348623157e+308") // 1.7976931348623157e+308
Use generic structure with Numeric protocol.
struct Example<T:Numeric> {
var string: String
init(number: T) {
self.string = "\(number)"
}
}
struct Example {
var string: String
init(number: Int) {
string = String(number)
}
init(number: Float) {
string = String(number)
}
init(number: Double) {
string = String(number)
}
}
You can have a look at how swift does this with its String-initializer:
struct Example {
init<Number>(number: Number) where Number: BinaryInteger {
string = String(number)
}
}

Filter array of nested structs

I'm looking for a nice swift solution for the following problem:
Lets say we have 2 structs like so:
struct Person {
let name: String
let age: Int
let skills: Skills
init(name: String, age: Int, skills: Skills) {
self.name = name
self.age = age
self.skills = skills
}
}
struct Skills {
let canUseBow: Bool
let canUseSword: Bool
let canUseShield: Bool
init(canUseBow: Bool, canUseSword: Bool, canUseShield: Bool) {
self.canUseBow = canUseBow
self.canUseSword = canUseSword
self.canUseShield = canUseShield
}
}
Now lets say I have an array of Person where each person has their own skills obviously where the corrosponding values can be true or false.
Lets say I want another array of just people that have the skill canUseBow as true so that skill must be set to true , how would I go about filtering out the Persons that do not have canUseBow set to true?
I was thinking in a direction of:
filteredPersons = persons.filter {
$0.skills
}
But that way it would require me to than select something after skills for example
$0.skills.canUseBow
That does not seem very future proof, lets say I would want to add more skills than I would also have to change the filter method again. Are there better ways to go about this?
You can try this with an OptionSet that can hold all of these flags for you in a simple Int storage.
import Foundation
struct Person {
let name: String
let age: Int
let skills: Skills
init(name: String, age: Int, skills: Skills) {
self.name = name
self.age = age
self.skills = skills
}
}
struct Skills: OptionSet {
let rawValue: Int
init(rawValue: Int) {
self.rawValue = rawValue
}
static let canUseBow = Skills(rawValue: 1 << 0)
static let canUseSword = Skills(rawValue: 1 << 1)
static let canUseShield = Skills(rawValue: 1 << 2)
init(json: [String: Bool]) {
var skills = Skills(rawValue: 0)
if let canUseBow = json["can_use_bow"], canUseBow {
skills.insert(.canUseBow)
}
if let canUseSword = json["can_use_sword"], canUseSword {
skills.insert(.canUseSword)
}
if let canUseShield = json["can_use_shield"], canUseShield {
skills.insert(.canUseShield)
}
self = skills
}
}
How to instantiate Skills?
let skills = Skills(json: [
"can_use_bow" : true,
"can_use_sword" : true,
"can_use_shield" : false,
])
How to filter based on multiple skills?
let targetSkills: Skills = [.canUseBow, .canUseSword]
let persons: [Person] = []
let filteredPersons = persons.filter {
targetSkills.isSubset(of: $0.skills)
}

Custom initiations in Swift - Is there a way to 'not' initialize certain properties in a method?

So my question is: Is there a way to create a custom initializer like below without initializing the other properties in the structure? I get an error within the custom initialization below because I am not giving values to the other two properties in the structure.
So my goal would be to create an instance of this structure, and only give values to three properties.
EX:
var starbucks = Coffee(caffeineAmountMg: 1200, countryOfOrigin: "South America", taste: "bad")
I just started coding a few days ago, so I apologize if there is a big lapse in my logic here, just trying to test out a few different things to make sure I understand everything I've learned so far.
struct Coffee {
var caffeineAmountMg: Int
init(caffeineAmountMg: Int) {
self.caffeineAmountMg = caffeineAmountMg
caffeineAmountG = caffeineAmountMg / 1000
}
var caffeineAmountG: Int
init(caffeineAmountG: Int) {
self.caffeineAmountG = caffeineAmountG
caffeineAmountMg = caffeineAmountG * 1000
}
var countryOfOrigin: String
var taste: String
}
One solution in this particular case is to store the quantity in a normalized form, and compute the other form(s). Let's say we store the quantity is milligrams. So:
struct Coffee {
init(caffeineAmountMg: Int, countryOfOrigin: String, taste: String) {
self.caffeineAmountMg = caffeineAmountMg
self.countryOfOrigin = countryOfOrigin
self.taste = taste
}
convenience init(caffeineAmountG: Int, countryOfOrigin: String, taste: String) {
self.init(caffeineAmountMg: caffeineAmountG * 1000, countryOfOrigin: countryOfOrigin, taste: taste)
}
var caffeineAmountMg: Int
var caffeineAmountG: Int {
get { return caffeineAmountMg / 1000 }
set { caffeineAmountMg = newValue * 1000 }
}
var countryOfOrigin: String
var taste: String
}
let cup = Coffee(caffeineAmountG: 1, countryOfOrigin: "South America is actually a continent", taste: "bad")
A variation on this style is to factor out the unit conversion into a separate type:
struct Mass {
let µg: Int
var mg: Int { return µg / 1_000 }
var g: Int { return µg / 1_000_000 }
private init(µg: Int) { self.µg = µg }
static func µg(_ µg: Int) -> Mass { return Mass(µg: µg) }
static func mg(_ mg: Int) -> Mass { return Mass(µg: mg * 1_000) }
static func g(_ g: Int) -> Mass { return Mass(µg: g * 1_000_000) }
}
struct Coffee {
init(caffeineAmount: Mass, countryOfOrigin: String, taste: String) {
self.caffeineAmount = caffeineAmount
self.countryOfOrigin = countryOfOrigin
self.taste = taste
}
var caffeineAmount: Mass
var countryOfOrigin: String
var taste: String
}
let cup = Coffee(caffeineAmount: .mg(10), countryOfOrigin: "South America is actually a continent", taste: "bad")
Yes, it can be done. The key to creating and initializing an object in Swift is to give all the variables values, i.e., initialize all of the object's variables to contain a valid value. The compiler will complain otherwise.
If you're not sure of the value of a certain variable, or if you would like to initialize a set of other variables, you can declare the unwanted variable as Optional, for ex.
var caffeineAmountG: Int?
An Optional variable means that it can either contain a valid value (Int in this case), or it can contain nil. In your init method, if you do not give any value to your Optional variable, it will be, by default initialised to nil automatically by the compiler.
So just as you wanted, you can have your struct initialised in a simple manner like so:
struct Coffee {
var caffeineAmountMg: Int
var caffeineAmountG: Int? //Considered caffeineAmountG = nil
var countryOfOrigin: String
var taste: String
init(caffeineAmountMg:Int, countryOfOrigin:String, taste:String) {
self.caffeineAmountMg = caffeineAmountMg
self.countryOfOrigin = countryOfOrigin
self.taste = taste
}
}
let coffee = Coffee(caffeineAmountMg: 32320, countryOfOrigin: "Country", taste: "Taste")
You can also - instead of initialising the variable to nil - provide any default value like this (without declaring it Optional):
var caffeineAmountG: Int = 0
Or you can compute it (caffeineAmountG) as you initialise your other variables:
init(caffeineAmountMg:Int, countryOfOrigin:String, taste:String) {
self.caffeineAmountMg = caffeineAmountMg
self.countryOfOrigin = countryOfOrigin
self.taste = taste
self.caffeineAmountG = self.caffeineAmountMg / 1000 //Converting Milligram to Gram
}
So according to the above init method, if we print the initialized object, it will contain the following values:
Coffee(caffeineAmountMg: 32320,
caffeineAmountG: 32,
countryOfOrigin: "Country",
taste: "Taste")
To follow up on #Paulw11 comment to use Measurement.
This keeps your exact API. You could improve things by making caffeineAmount public, and getting rid of caffeineAmountMg or caffeineAmountG.
struct Coffee {
var caffeineAmountMg: Int {
get { return Int(caffeineAmount.converted(to: UnitMass.milligrams).value) }
set { caffeineAmount = Measurement(value: Double(newValue), unit: UnitMass.milligrams) }
}
init(caffeineAmountMg: Int, countryOfOrigin: String, taste: String) {
self.caffeineAmount = Measurement(value: Double(caffeineAmountMg), unit: UnitMass.milligrams)
self.countryOfOrigin = countryOfOrigin
self.taste = taste
}
var caffeineAmountG: Int {
get { return Int(caffeineAmount.converted(to: UnitMass.grams).value) }
set { caffeineAmount = Measurement(value: Double(newValue), unit: UnitMass.grams) }
}
init(caffeineAmountG: Int, countryOfOrigin: String, taste: String) {
caffeineAmount = Measurement(value: Double(caffeineAmountG), unit: UnitMass.grams)
self.countryOfOrigin = countryOfOrigin
self.taste = taste
}
var countryOfOrigin: String
var taste: String
private var caffeineAmount: Measurement<UnitMass>
}

Array of structs: UserDefaults, how to use?

I've already check all of those topics:
How to save an array of custom struct to NSUserDefault with swift?
How to save struct to NSUserDefaults in Swift 2.0
STRUCT Array To UserDefaults
I have a struct containing some Strings and an other struct: MySection.
struct MySection {
var name: String = ""
var values: [MyRow] = []
}
And there is MyRow which is store in MySection.values
struct MyRow {
var value: String = ""
var quantity: String = ""
var quantityType: String = ""
var done: String = ""
}
Two arrays for use it
var arraySection: [MySection] = []
var arrayRow: [MyRow] = []
And in my application, I add dynamically some values in those arrays.
There is the delegate method for get datas from my second ViewController
func returnInfos(newItem: [MyRow], sectionPick: String) {
arrayRow.append(MyRow())
arrayRow[arrayRow.count - 1] = newItem[0]
manageSection(item: sectionPick)
listTableView.reloadData()
}
And there is the manageSection function.
func manageSection(item: String) {
var i = 0
for _ in arraySection {
if arraySection[i].name == item {
arraySection.insert(MySection(), at: i + 1)
arraySection[i + 1].values = [arrayRow[arrayRow.count - 1]]
return
}
i += 1
}
arraySection.append(MySection())
arraySection[arraySection.count - 1].name = item
arraySection[arraySection.count - 1].values = [arrayRow[arrayRow.count - 1]]
}
My need is to store datas of the two arrays in UserDefaults (or CoreData maybe??) and use these datas when the user going back to the application.
I don't know how to do it, I've already try methods from the 3 topics but I'm not even doing a good job.
How can I do it?
Thanks guys!
Since both types contain only property list compliant types a suitable solution is to add code to convert each type to a property list compliant object and vice versa.
struct MySection {
var name: String
var values = [MyRow]()
init(name : String, values : [MyRow] = []) {
self.name = name
self.values = values
}
init(propertyList: [String: Any]) {
self.name = propertyList["name"] as! String
self.values = (propertyList["values"] as! [[String:String]]).map{ MyRow(propertyList: $0) }
}
var propertyListRepresentation : [String: Any] {
return ["name" : name, "values" : values.map { $0.propertyListRepresentation }]
}
}
struct MyRow {
var value: String
var quantity: String
var quantityType: String
var done: String
init(value : String, quantity: String, quantityType: String, done: String) {
self.value = value
self.quantity = quantity
self.quantityType = quantityType
self.done = done
}
init(propertyList: [String:String]) {
self.value = propertyList["value"]!
self.quantity = propertyList["quantity"]!
self.quantityType = propertyList["quantityType"]!
self.done = propertyList["done"]!
}
var propertyListRepresentation : [String: Any] {
return ["value" : value, "quantity" : quantity, "quantityType" : quantityType, "done" : done ]
}
}
After creating a few objects
let row1 = MyRow(value: "Foo", quantity: "10", quantityType: "Foo", done: "Yes")
let row2 = MyRow(value: "Bar", quantity: "10", quantityType: "Bar", done: "No")
let section = MySection(name: "Baz", values: [row1, row2])
call propertyListRepresentation to get a dictionary ([String:Any]) which can be saved to User Defaults.
let propertyList = section.propertyListRepresentation
Recreation of the section is quite easy, too
let newSection = MySection(propertyList: propertyList)
Edit
Use the propertyList initializer only if you get data from UserDefaults in all other cases use the other initializer.
For example replace
#IBAction func addButtonPressed(_ sender: Any) {
newProducts.append(MyRow(propertyList: ["":""]))
newProducts[newProducts.count - 1].value = nameTextField.text!
newProducts[newProducts.count - 1].quantity = quantityTextField.text!
newProducts[newProducts.count - 1].quantityType = type
newProducts[newProducts.count - 1].done = "No"
delegate?.returnInfos(newItem: newProducts, sectionPick: typePick)
navigationController?.popViewController(animated: true)
}
with
#IBAction func addButtonPressed(_ sender: Any) {
let row = MyRow(value: nameTextField.text!,
quantity: quantityTextField.text!,
quantityType: type,
done: "No")
newProducts.append(row)
delegate?.returnInfos(newItem: newProducts, sectionPick: typePick)
navigationController?.popViewController(animated: true)
}
and replace
func returnInfos(newItem: [MyRow], sectionPick: String) {
arrayRow.append(MyRow(propertyList: ["":""]))
arrayRow[arrayRow.count - 1] = newItem[0]
manageSection(item: sectionPick)
listTableView.reloadData()
}
with
func returnInfos(newItem: [MyRow], sectionPick: String) {
arrayRow.append(newItem[0])
manageSection(item: sectionPick)
listTableView.reloadData()
}
Basically first create the object, then append it to the array. The other way round is very cumbersome.

Error:Missing argument for parameter 'makePetMakeNoise' in call

Hi I am doing some Swift coding and I cant figure out how to get rid of this error:
Missing argument for parameter 'makePetMakeNoise' in call.
Could you help me fix this error?
import Foundation
import UIKit
class Human {
static var numCreated:Int = 4
var name:String = ""
var pet:Pet
init(name:String,pet:Pet){
self.name = name
self.pet = pet
Human.numCreated++
}
func makePetMakeNoise(){
var randomNumber = arc4random_uniform(9)
self.pet.makeNoise(randomNumber) //Missing argument for parameter 'makePetMakeNoise' in call
}
func feedPet(){
self.pet.eat
}
static func populationCount(){
println("Total population count is \(Human.numCreated)")
}
}
class Pet {
var name:String = ""
var noise:String = ""
var canMakeNoise:Bool = true
init(name:String,noise:String,canMakeNoise:Bool){
self.name = name
self.noise = noise
self.canMakeNoise = canMakeNoise
}
func makeNoise(canMakeNoise: Int, makePetMakeNoise: Int){
if self.canMakeNoise {
for _ in 1...5{
println("\(self.name) \(self.noise)")
}
}else {
println("\(self.name) *remains silent*")
}
}
func eat(){
println("\(name) is eating")
}
class Dog:Pet{
}
class Cat:Pet{
override func eat {
super.eat()
println("I'm still hungry, meow")
}
}
}
//Pets
var Tobie = Pet(name: "Tobie", noise: "Bark", canMakeNoise: true)
var Bud = Pet(name: "Bud", noise: "Bark", canMakeNoise: false)
var Ginger = Pet(name: "Ginger", noise: "bark", canMakeNoise: false)
var Curry = Pet(name: "Curry", noise: "Bark", canMakeNoise: true)
//Humans
var Sam = Human(name: "Sam", pet: Tobie)
var Mark = Human(name: "Mark", pet: Bud)
var Spencer = Human(name: "Spencer", pet: Ginger)
var Jessie = Human(name: "Jessie", pet: Curry)
let Humans = [Sam, Mark, Spencer, Jessie]
for Human in Humans {
println("\(Humans) \(feedPet) \(makePetMakeNoise)") //Use of unresolved identifier 'feedPet and makePetMakeNoise'\\
}
`
Your makeNoise method defined for Pet takes two parameters. Problem is that when you call it in the line that gives you the error, you pass it just one parameter.
Possible solution is:
Change the Pet's method as follow:
func makeNoise(makePetMakeNoise: Int){
if self.canMakeNoise {
for _ in 1...5{
println("\(self.name) \(self.noise)")
}
}else {
println("\(self.name) *remains silent*")
}
}
Update the code that gives you the error in Human like this:
func makePetMakeNoise(){
var randomNumber = Int(arc4random_uniform(9))
self.pet.makeNoise(randomNumber)
}
Notice I convert random to Int since this is the expected parameter type.
Please consider I'm just guessing a possible modification since I don't know what your classes are intended for ... maybe just a playground.
Other solution is effectively passing two parameters when you call the pet's method from human.
Hope this helps
Your function func makeNoise(canMakeNoise: Int, makePetMakeNoise: Int) expects two arguments, but your are only passing the one randonNumber in your call self.pet.makeNoise(randomNumber). It should look something like this: self.pet.makeNoise(randomNumber, someOtherNumber)