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.
Related
So I'm trying to make a List in SwiftUI like this:
struct DetailsView: View {
var piis = [IDPiece]()
var body: some View {
List(piis, id: \.identifier) { pii in
Text( pii.label )
}
}
}
where IDPiece looks like:
struct IDPiece: Equatable {
init() {}
init(claim: Claim) {
self.document = claim.document
self.identifier = claim.identifier
self.claimUID = claim.claimUID
self.label = claim.label
}
var document: DocumentType = .na
var identifier: String = ""
var claimUID: String = ""
var label: String?
}
But I keep receiving the following error on the line where I initialize the list:
Type '_' has no member 'identifier'
It doesn't seem to be parsing the type of object contains in my piis list. Anyone know why that may be?
SwiftUI compiler errors are generally useless (this will improve over time, but today they're useless). Your problem has nothing to do with \.identifier. The problem is you have an optional .label, but you don't handle the case where it's nil. Almost certainly you should just make label non-optional. But if it needs to be optional (if you treat nil differently than empty in some place), then you need to do something about that, such as:
Text(pii.label ?? "N/A")
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
In my app I have two struct arrays and I want to remove common items from one of them. My struct:
struct PeopleSelectItem {
var name = ""
var id = ""
var added = false
}
My arrays:
var people : [PeopleSelectItem] = []
var selectedPeople : [PeopleSelectItem] = []
I want to remove items from people array if they exist (compare by id) on selectedPeople array.
I tried several array filtering and converting to set but none of them worked. What can I do here?
Thanks!
Get an array of all ids in selectedPeople
let selectedPeopleIDs = selectedPeople.map(\.id)
Filter the items whose id is not in the array
let filteredPeople = people.filter { !selectedPeopleIDs.contains($0.id) }
If you know that people equal each other if the id is the same then you can conform your struct to the Equatable protocol and you can use the array filter method.
struct PeopleSelectItem : Equatable {
var name = ""
var id = ""
var added = false
}
func ==(lhs: PeopleSelectItem, rhs: PeopleSelectItem) -> Bool {
return lhs.id == rhs.id
}
func filterPeople() {
//swift 2, 3:
people = people.filter{!selectedPeople.contains($0)}
//swift older versions:
people = people.filter{!contains(selectedPeople, $0)}
}
If people might have a significant amount of entries, performance should be considered. So, instead of searching with an n^2 algorithm, you should make use of Swifts dictionary and the corresponding hash-search to find items.
If Id is unique for people then I would store them in a dictionary like:
var peopleDict: [String:PeopleSelectItem] = [:]()
You can easily convert from the array you have to this dictionary:
people.foreach {peopleDict[$0.id] = $0}
With this dictionary it's very easy to delete single entries:
selectedPeople.foreach {peopleDict.remove($0.id)}
Optionally to switch back to an array for people you just say:
let filteredPeople = peopleDict.values as [PeopleSelectItem]
Remarks
I assumed, that selectedPeople is smaller then the base of all people. If this is not the case, you should pu selectedPeople in a dictionary.
did I say I like this Spark like api? I think I do so.
I just wrote that code from top of my mind. If it is not completely syntactically correct let me know and I correct it.
I have made a simple struct and implemented the Equatable protocol :
extension MyModelStruct: Equatable {}
func ==(lhs: NModelMatch, rhs: NModelMatch) -> Bool {
let areEqual = lhs.id == rhs.id
return areEqual
}
public struct MyModelStruct {
var id : String?
var staticId : String?
init(fromDictionary dictionary: NSDictionary){
id = dictionary["id"] as? String
...
}
Then in my project i get an array of [MyModelStruct], what i what to do is to remove all the MyModelStruct that have the same id
let val1 = MyModelStruct(id:9, subId:1)
let val2 = MyModelStruct(id:10, subId:1)
let val3 = MyModelStruct(id:9, subId:10)
var arrayOfModel = [val1,val2,val3]; // or set but i do not know how to use a set
var arrayCleaned = cleanFunction[M2,M3]
How can i make the cleanFunction ?
Can someone help please.
Thanks for all.
Xcode : Version 7.3.1
I really don't want people to just take an answer because it's the only one, that's why I'm showing you how you can use the power of sets. Sets are used wherever it doesn't make sense to have more than one, either it's there or not. Sets provide fast methods for checking whether an element is in the set (contains), removing an element (remove), combining two sets (union) and many more. Often people just want an array because they're familiar with it, but often a set is really what they need. With that said, here is how you can use a set:
struct Model : Hashable {
var id : String?
var hashValue: Int {
return id?.hashValue ?? 0
}
}
func ==(l: Model, r: Model) -> Bool {
return l.id == r.id
}
let modelSet : Set = [
Model(id: "a"),
Model(id: "hello"),
Model(id: "a"),
Model(id: "test")
]
// modelSet contains only the three unique Models
The only requirement for a type in order to be in a set is the Hashable protocol (which extends Equatable). In your case, you can just return the underlying hashValue of the String. If your id is always a number (which it probably is), you should change the type of id to be an Int, because Strings are much less efficient than Ints and it doesn't make sense to use a String.
Also consider storing this property somewhere, so that every time you receive new models, you can just do
modelSet.unionInPlace(newModels)
Use a Set instead of an Array.
I agree you are better off using a Set. You should be able to initialize the Set using an Array, e.g., var arrayOfModel: Set = [val1, val2, val3]. But because you are using a custom type, you will need to make sure that MyModelStruct conforms to hashable. This link has a good explanation.
But if you want to use an array then you need to change
let areEqual = rhs.id == lhs.id
to
let areEqual = rhs.id == lhs.id && rhs.subId == lhs.subId)
You need to modify your struct to have a subId property (and make the variables Int instead of String.
In answer to your question, yes you do need to iterative over the array.
If you extend the Array type with this function :
extension Array
{
func uniqueValues<V:Equatable>( value:(Element)->V) -> [Element]
{
var result:[Element] = []
for element in self
{
if !result.contains({ value($0) == value(element) })
{ result.append(element) }
}
return result
}
}
You'll be able to clean up duplicates using :
var arrayCleaned = arrayOfModel.uniqueValues({$0.id!})
without needing to make the structure Equatable.
Please note that this is not going to be efficient if your array is very large and you might want to consider filtering insertions into your array at the source if possible.
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.