I'm having issues with appending an optional value to an array in Swift. The view I'm writing is for the creation of a routine for the gym. However my Routine object is not being instantiated as it should be.
I have experience with other programming languages but I am fairly new to Swift, and optionals.
My ViewController contains an optional variable:
var routine: Routine?
Where the Routine class contains:
name: String
exerciseList: [String]()
numOfSets: [Int]()
When I am preparing it to send the newly created routine to my other ViewController, I take the values from user input to edit the fields of the object.
let name = routineName.text ?? ""
let numberOne = Int(numOfSetsOne.text ?? "0") //numOfSetsOne is a text label
routine?.exerciseList.append(selectedExerciseOne!) //Haven't tested to see if this works yet
routine?.numOfSets[0] = numberOne! //This line is not working
routine = Routine(name: name)
To try a little debugging I put print statements on either side of the line like so:
print ("numberOne Value: \(numberOne!)")
routine?.numOfSets[0] = numberOne!
print ("numOfSets[0] Value: \(routine?.numOfSets[0])")
I expected the output from the second print statement to be identical to the first. However the terminal output:
numberOne Value: 3
numOfSets[0] Value: nil
Does anyone know what has gone wrong here?
Thanks
You have declared a property that may contain a Routine, but you have not assigned an instance of Routine to that property before trying to use it.
This means that, for example,
routine?.numSets[0] = numberOne!
doesn't do anything - routine is nil and so the statement is skipped.
You should create an appropriate init function for your Routine class and use that to create a new Routine and assign it to routine
For example:
class Routine {
var name: String
var exerciseList = [String]()
var numberOfSets = [Int]()
init(named: String) {
self.name = named
}
}
Then you can say
let name = routineName.text ?? ""
let numberOne = Int(numOfSetsOne.text ?? "0")
self.routine = Routine(named: name)
self.routine?.numberOfSets.append(numberOne!)
Coordinating related arrays can get a bit messy, so I would use a single array:
struct ExerciseSet {
let exerciseName: String
let sets: Int
}
class Routine {
var name: String
var exerciseList = [ExerciseSet]()
init(named: String) {
self.name = named
}
}
Your Routine is not initialised before its being assigned value
try
let name = routineName.text ?? ""
let numberOne = Int(numOfSetsOne.text ?? "0")
routine = Routine(name: name)
routine?.exerciseList.append(selectedExerciseOne!)
routine?.numOfSets[0] = numberOne!
Related
I have a swift array of struct and I am unable edit the first property, whereas I am able edit the first property with an array of class.
In order to edit the first object of the struct array, I have to do [0] then .first
I know structs are valued by type, class are value by reference. But I don't understand the different behavior. Can someone explain?
class PersonObj {
var name = "Dheearj"
}
struct Person {
var name = "Dheearj"
mutating func update(name: String){
self.name = name
}
}
var array = [Person(),Person()]
array[0].update(name:"dheeraj")
array[0].name = "yuuu"
array.first?.name = "dddddd" <--- "Error Here"
var array1 = [PersonObj(),PersonObj()]
array1.first!.name = "ttt"
print(array1.first?.name ?? "")
print(array.first?.name ?? "")
print(array.count)
Screenshot of the error message:
Mutating a struct stored within some other property behaves as though you've copied out the value, modified it, and overwrote it back into place.
Take this line for example: (I replaced the optional chaining with force unwrapping, for simplicity)
array.first!.name = "dddddd"
It behaves as though you did:
var tmp = array.first!
tmp.name = "dddddd"
array.first = tmp
It's easy to see what that doesn't work. Array.first, is a get-only property (it doesn't have a setter).
The case for classses works because the value stored in the array is a reference to the object, and the reference isn't changing (only the values within the object it refers to, which the array doesn't know or care about).
for eg:-
In class DishPostedVC, I have an array of model:-
var finalDatesOfDish : [DishActivationDateTimeModel]?
If i pass this variable (finalDatesOfDish) to class DishActivationVC, and do some deletion operation in class DishActivationVC, and if i go back again to class DishPostedVC, then i am getting data after deletion not the actual data, but i am just coming back not passing any data.
I don't know why but this situation occurred one more time earlier with model array, its so strange, how's it possible?
can u tell me what shld i do?
class DishActivationDateTimeModel {
var dayDate: Date? = nil
var dayDateStr: String = ""
var servingsLeft: String = ""
var firstSlotPostedDishId: String = ""
var secondSlotPostedDishId: String = ""
var startTimeDateForFirstSlot: Date? = nil
var startTimeStringForFirstSlot: String = ""
var endTimeDateForFirstSlot: Date? = nil
var endTimeStringForFirstSlot: String = ""
var startTimeDateForSecondSlot: Date? = nil
var startTimeStringForSecondSlot: String = ""
var endTimeDateForSecondSlot: Date? = nil
var endTimeStringForSecondSlot: String = ""
}
It's my model
class DishActivationDateTimeModel {
var dayDate: Date? = nil
var dayDateStr: String = ""
var servingsLeft: String = ""
var firstSlotPostedDishId: String = ""
var secondSlotPostedDishId: String = ""
var startTimeDateForFirstSlot: Date? = nil
var startTimeStringForFirstSlot: String = ""
var endTimeDateForFirstSlot: Date? = nil
var endTimeStringForFirstSlot: String = ""
var startTimeDateForSecondSlot: Date? = nil
var startTimeStringForSecondSlot: String = ""
var endTimeDateForSecondSlot: Date? = nil
var endTimeStringForSecondSlot: String = ""
init(_ object : DishActivationDateTimeModel) {
self.dayDate = object.dayDate
self.dayDateStr = object.dayDateStr
// ....
// ....
self.endTimeStringForSecondSlot = object.endTimeStringForSecondSlot
}
}
extension Array where Element : DishActivationDateTimeModel {
func copyModelArray() -> [DishActivationDateTimeModel] {
var array : [DishActivationDateTimeModel] = []
for object in self {
array.append(DishActivationDateTimeModel(object))
}
return array
}
}
If you don't want to make changes in your main data then copy your
model array using copyModelArray method. And use your operational
model array. So, its not affect to your main model data array.
In Swift structs and classes give you both value and reference-based constructs for your objects. Structs are preferred for objects designed for data storage like Array. Structs also help remove memory issues when passing objects in a multithreaded environment. Classes, unlike structs, support inheritance and are used more for containing logic like UIViewController. Most standard library data objects in Swift, like String, Array, Dictionary, Int, Float, Boolean, are all structs, therefore value objects. The mutability of var versus let is why in Swift there are no mutable and non-mutable versions of collections like Objective C’s NSArray and NSMutableArray.
when you passed around your structs its copied but when you passed or assigned classes it gets reference to it.This means when you change class object in one location it changes everywhere, when you affect structs it changes individually. if you want more information differences between classes and structs you can check araound my article.
classes versus structs
just a bit of hindsight for you to understand my problem. I am currently coding an iOS app for events where I have a GuestList CoreData saved on device.
On viewDidLoad, it will fetch the coredata object and place it into an array of [GuestDetails]
Now the Guestdetail object struct is as follows:
private(set) public var guestFirstName: String
private(set) public var guestLastName: String?
private(set) public var guestEmail: String?
private(set) public var guestPhone: String?
private(set) public var guestUUID: UUID
private(set) public var guestBarcode: String?
private(set) public var guestCheckedIn: Bool
Such that only first name, UUID and checkinStatus are compulsory. I have already set up adding by JSON but now my issue is on my GuestListViewController, I have a searchbar
I am using the following code and array to make sure I can filter.
I have another array that is called
searchResultArray = [GuestDetails]()
So essentially I would copy all my guest details to searchResultArray and that is the one that the tableView is getting its sources from.
Now as part of the search, I used this code which I found on appcoda
searchResultArray = guestData.filter({guestData -> Bool in
(guestData.guestFirstName.lowercased().contains(searchText.lowercased())) ||
(guestData.guestLastName?.lowercased().contains(searchText.lowercased()))! ||
(guestData.guestBarcode?.contains(searchText))!
})
The issue is that my app is crashing, if I only search by first name it will not crash since those are force unwrapped nicely, but if I add the code to search lastName or Barcode, it will crash. I understand that it is probably because of explicitly unwrapped but xcode would not let me NOT unwrap it.
I have tried using map (which does not help, unless i am not familiar enough with it),
I have tried .compact (but I could not get it to work as i am not sure how it can access the inside of an GuestDetail object to remove nils)
The issue is the array of [GuestDetails] itself will not contain null some details inside a GuestDetails object might, hence causing it to crash.
My question is, how do I get it to search by firstname(already possible), lastname and barcode?
Thanks and I hope the question was elaborate enough.
I would avoid using forced unwrapping like so:
searchResultArray = guestData.filter({guestData -> Bool in
let searchLowercased = searchText.lowercased()
if guestData.guestFirstName.lowercased().contains(searchLowercased) {
return true
}
if let guestLastName = guestData.guestLastName, guestLastName.lowercased().contains(searchLowercased) {
return true
}
if let guestBarcode = guestData.guestBarcode, guestBarcode.lowercased().contains(searchLowercased) {
return true
}
return false
})
Answering your question from comments: You don't have to use ? operator on optionals because we use Optional Binding if let syntax. So for example:
if let guestLastName = guestData.guestLastName if guestData.guestLastName is nil than we will just jump out of this if statement. then you see , in the if statement, we will go pass this comma only if guestData.guestLastName is not nil, that is why we can use the guestLastName variable that is unwrapped String and is no longer optional String?, we than proceed to check if search term matches the guestLastName and return true.
Please read: if let , if var, guard let,guard var and defer statements in swift
It would be even better if you happen to add another property to your Data and avoid doing all this if else you can do something like:
searchResultArray = guestData.filter({guestData -> Bool in
let searchLowercased = searchText.lowercased()
let matches:[String?] = [guestData.guestFirstName, guestData.guestLastName, guestData.guestBarcode]
let nonNilElements = matches.compactMap { $0 }
for element in nonNilElements {
if element.lowercased().contains(searchLowercased) {
return true
}
}
return false
})
It's an interesting little problem, so let's generalize it. Here's our test data, comparable to your array of GuestDetails:
struct S {
var s1 : String
var s2 : String?
var s3 : String?
}
var array = [S]()
array.append(S(s1: "test", s2: "yo", s3: "ha"))
array.append(S(s1: "test", s2: nil, s3: nil))
array.append(S(s1: "Howdy", s2: "Bonjour", s3: "Hello"))
let target = "hello"
Some S properties are Optional, others are not.
So the problem is: Filter array down to only those elements where any S property contains our target string, using case insensitive comparison.
We can do that in one statement:
let filteredArray = array.filter {
[$0.s1,$0.s2,$0.s3].compactMap {$0}
.map {$0.localizedCaseInsensitiveContains(target)}
.contains(true)
}
It's not quite as efficient as what #Ladislav wrote, because we keep looping inside map even after we've found our string. But the inefficiency is probably not significant.
Sorry if the title is rather confusing, but I'm curious to know the difference between these two lines:
var title = String()
var title: String
Is one being initialized and one only be declared? Which is more correct?
For example, if I have a struct should I use one of the other?
So the reason I ask this is because I'm learning about how to grab some JSON from a url and then display it in my app. One of the new ways of doing so is using Decodable. So, I have a struct in a model class like so:
struct Videos: Decodable {
var title = String()
var number_of_views : Int
var thumbnail_image_name: String
var channel: Channel
var duration: Int
}
In another class I have this:
URLSession.shared.dataTask(with: url){(data,response,error) in
if(error != nil){
print(error!)
return
}
guard let data = data else { return }
do{
self.Videos2 = try JSONDecoder().decode([Videos].self, from: data)
//self.collectionView?.reloadData()
}catch let jsonErr{
print(jsonErr)
}
}.resume()
So, should I declare or initialize the variables in my struct? I'm assuming I should just declare them like so:
var title: String?
Would that be the correct syntax in my struct?
UPDATE:
I understand this question was more broad then I originally proposed it to be. I'm sorry about that, but thank you so much for all your great answers that clarified a lot up for me.
The difference is that : defines the type of your variable, whereas = assigns an actual value to the variable.
So:
var title = String()
This calls the initializer of the String type, creating a new String instance. It then assigns this value to title. The type of title is inferred to be String because you're assigning an object of type String to it; however, you could also write this line explicitly as:
var title: String = String()
This would mean you are declaring a title variable of type String, and assigning a new String to it.
var title: String
This simply says you're defining a variable of type String. However, you are not assigning a value to it. You will need to assign something to this variable before you use it, or you will get a compile error (and if this is a property rather than just a variable, you'll need to assign it before you get to the end of your type's init() method, unless it's optional with ? after it, in which case it gets implicitly initialized to nil).
EDIT: For your example, I'd probably declare all the variables using let and :, assuming that your JSON provides values for all of those properties. The initializer generated by Decodable should then set all the properties when you create the object. So, something like:
struct Videos: Decodable {
let title: String
let number_of_views : Int
let thumbnail_image_name: String
let channel: Int
let duration: Int
}
This initializes a value
var title = String()
This declares a value but does not initialize it
var title: String
If you attempt to use the latter, such as print(title), you will get a compiler error stating Variable 'title' used before being initialized
It does not matter whether the value is a class or a struct.
The = operator is the assignment operator, it assigns a value to the object on the left of the =
Typically, class or struct properties are declared but not initialized until the init() is called. A simple class might be
class MyClass {
let myProperty: String
init(aString: String) {
self.myProperty = aString
}
}
Whereas inside the scope of a function you may declare a local variable that only lives inside the scope of the function.
func doSomethingToAString(aString: String) -> String {
let extraString = "Something"
let amendedString = aString + extraString
return amendedString
}
In your specific example, the struct synthesizes an initializer that will allow you to initialize the struct with all the values needed to fill your properties. The initializer generated by Decodable should then set all the properties when you create a Videos struct, you will do it something like:
let aVideos = Videos(title: "My Title", number_of_views: 0, thumbnail_image_name: "ImageName", channel: Channel(), duration: 10)
Is one being initialized and one only be declared?
Yes, meaning that the declared cannot be used. If you tried to set a value for it, you would get a compile-time error:
variable 'title' passed by reference before being initialized
Which is more correct?
There is no rule of thumb to determine which is more correct, that would be depends on is there a need to initialize title directly.
On another hand, when it comes to declare properties for a class, saying var title = String() means that you are give title an initial value ("") which means that you are able to create an instance of this class directly, example:
class Foo {
var title = String()
}
let myFoo = Foo()
However, if title declared as var title: String, you will have to implement the init for Foo:
class Foo {
var title: String
init(title: String) {
self.title = title
}
}
let myFoo = Foo(title: "")
Also, you have an option to declare it as lazy:
lazy var title = String()
which means:
A lazy stored property is a property whose initial value is not
calculated until the first time it is used. You indicate a lazy stored
property by writing the lazy modifier before its declaration.
Properties - Lazy Stored Properties
I have a loop like so that creates a string representing a url:
for(var i = 1; i < 6; i++)
{
let urlString: String = "http://{...}/data/\(i).txt"
var downloader = FileDownloader(url: urlString, array: peopleArray, table: theTable)
downloaderQueue.addOperation(downloader)
}
FileDownloader constructor is as follows:
let urlString: String
var personArray: Array<Person> = []
var person: Person
let table: UITableView
init(url: String, array: Array<Person>, table: UITableView)
{
self.urlString = url
self.person = Person()
self.personArray = array
self.table = table
}
When this code runs, lldb gives me the error:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
And I know the problem is the string because of the debugger output:
downloader Lecture_14.FileDownloader 0x000000016fd89f60 0x000000016fd89f60
Foundation.NSOperation NSOperation
urlString String "unexpectedly found nil while unwrapping an Optional value"
_core _StringCore
Any ideas why this would be happening?
In Xcode, option-click on each of the variables in use: urlString, peopleArray and theTable.
The popup that appears will show you whether the variable is an optional variable by appending a ? to the class name.
From your code above, urlString should not be an optional and therefore should not be the problem. But check the other variables in use and see if any of them are optionals.
If so, use something like this:
if let checkedPeopleArray = peopleArray {
// now you can use checkedPeopleArray and be sure it is not nil
}
A couple of other points to make your code more Swift-like:
Your loop can be written like this, using Swift's range instead of the traditional C-style loop:
for i in 1..<6 {
let urlString: String = "http://{...}/data/\(i).txt"
}
When declaring an array, Apple changed this from the first version of Swift. Instead of:
var personArray: Array<Person> = []
try:
var personArray: [Person]() // empty array for Person objects
And in your init:
init(url: String, array: [Person], table: UITableView)
Functionally the same, but I feel it is better to use the changes to the language as they appear because there is no telling when/if Apple might remove the old syntax.