Swift - JSON Decode into Struct Contains Against String - arrays

I have a JSON file I am grabbing from a remote server and decoding it. It is being decoded into a struct and then used in an #Published var.
For example:
JSON:
[{"keyword": "foo"}, {"keyword": "blah"}]
Struct:
struct keywords: Codable {
var keyword: String
}
Observable Class:
#Published var keys: [keywords] = []
I need to do a real-time comparison, using contains, against a value the user is entering, utilizing an if statement. I can get it to use the first entry in the keys var and check against any characters that might be in that string, but I cannot get it to work across the entire array on only the full strings (not individual characters).
Here's what correctly checks against only the first entry of the array and each individual character.
if keys[0].keyword.contains(where: blah.contains)
I have also tried mapping it to strings like this (does exactly the same thing):
if (keys[0].keyword.map{String($0)}).contains(where: blah.contains)
Been at this all day but cannot find any docs on how to do this correctly.
The goal is to not just have it use the first entry of the array but the entirety of the entries of the array. I understand that is the [0] but it wouldn't compile without it. I need to make it compare on the entire string, not on individual characters.
For example, if the word blah is the array and the user enters bla it should not match anything. Only if the entire word blah is contained in the user's entry should it match (i.e. fooblahfoo would match because blah is within the string, but foobalbabh would NOT match even though all of the characters contained within the word blah are in that string).
Appreciate any assistance with actual code examples. This is Swift 5.5.
UPDATE:
Not sure where the confusion is coming from but here's an even clear explanation.
There is a TextField called username where the user enters a string.
If the user enters the string fooblahfoo and the word blah is somewhere in the array it should match.
This is easy when you have a simple array of strings like this:
keywords = ["foo", "blah"]
For that you just do keywords.contains(where: username.contains)
However, the decoded JSON is more of a dictionary than a simple array of strings, so you have to call to the key of keyword and look at all the values in order to make the comparison.

First, use contains(where:) on the keys array -- then, check each item to see if the user input contains that keyword.
struct Keywords: Codable {
var keyword: String
}
var keys: [Keywords] = [.init(keyword: "foo"),.init(keyword: "blah")]
var userInput = "fooblahfoo"
var result = keys.contains { userInput.contains($0.keyword) }
print(result)
true

Related

Error when trying to set array in userdefaults: Thread 1: "Attempt to insert non-property list object

I have solved the issue now, thanks for your help. I shouldn't have tried to save arrays with UITextViews, but I should have saved their text as strings instead. Here was the original question:
I have tried a lot, and googled a lot, but I can't solve this problem on my own. Whenever I try to save an array in userdefaults, it just is not working. I get the following error:
Thread 1: "Attempt to insert non-property list object (\n "<UITextView: 0x14001f800; frame = (0 0; 355 180); text = 'D'; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x600003f01d10>; layer = <CALayer: 0x6000031c83e0>; contentOffset: {0, 0}; contentSize: {355, 30}; adjustedContentInset: {0, 0, 0, 0}>"\n) for key content"
I don't know what a non-property list object is. And I do not know how to solve the problem. Below is the lines of code that do not work.
var contentList: [Any] = []
let cl = defaults.array(forKey: "content")!
if cl.count != 0{
contentList += cl
}
contentList.append(label)
defaults.setValue(contentList, forKey: "content")
If I take out the last line of code by turning it into a comment everything runs just fine. How should I replace that line of code? I essentially want to save an array of UITextViews and make it larger every time I call a fucntion (this code is part of a larger function). The reason why I have created another two lists (cl and contentList) is that it helps me with a problem down the line. What I cannot understand however, is why the last line of code doesn't work. If anyone has any ideas, please help me, it would be much appreciated.
Use only String as stated in comments :
var contentList: [String] = []
let cl = defaults.array(forKey: "content")!
if cl.count != 0{
contentList += cl
}
If lbText = label.text {
contentList.append(lbText)
defaults.setValue(contentList, forKey: "content")
}
You can only store a very limited list of data types into UserDefaults, commonly referred to as "property list objects" (Since property list (or plist) files will only store the same data types.
To quote the Xcode docs on UserDefaults, in the section titled "Storing Default Objects":
A default object must be a property list—that is, an instance of (or for collections, a combination of instances of) NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary [or Data, String, NSNumber, Date, Array, or Dictionary types in Swift.] If you want to store any other type of object, you should typically archive it to create an instance of Data.
(I added the equivalent Swift types to the above quote in square brackets, since it looks like Apple hasn't updated it for Swift.)
That's worded a little awkwardly. The idea is that you can only store data of the types listed. Because the Array and Dictionary types are "container" types, you can store any combination of arrays and dictionaries that contain combinations of any of the above types. For example, you can store an array that contains a dictionary, 3 dates, 2 floats, a Double, some Data, and 2 arrays, and those dictionaries and arrays can contain other dictionaries and/or arrays.)
It is almost always wrong to archive UIView objects like UITextViews. You should save the text properties of your text views instead.
If you want to manage a vertical stack of UITextView objects, I suggest adding a vertical stack view to your user interface, and then writing code that adds or removes UITextView subviews to your stack view. You should be able to find plenty of examples of adding and removing objects from stack views online. (It's really easy.)
If you want to manage a scrolling list of feeds of arbitrary length, you might want to use a table view or collection view instead. Those require that you set up a data model and implement a "data source". That takes a little practice to get right, but is very powerful.

Displaying text from nested multi-dimensional ForEach in SwiftUI

I am fairly new to SwiftUI but am pulling my hair out trying to display text from two ForEach loops. I am working on a song app that would display lyrics in stanzas. My data is one array Lyrics that holds Verses which is another array. Each verse is a stanza. And each verse has a string array that stores the lyrics for one line.
//Lyrics variable
#Published var lyrics: [Verse]? = [Verse]()
//Verse structure storing each line
struct Verse: Codable, Identifiable {
let id = UUID()
let verseContent: [String]
}
The part I am having trouble is in implementation of getting all of the information into Text within my View. One ForEach loop works and I can get the first line of each of my stanzas as follows
//Builds Fine with this code
ForEach(lyrics) { verse in
Text(verse.verseContent[0])
}
But the problem is when I try and do a nested ForEach to get all of the lines in each stanza with the following.
return AnyView(
ForEach(lyrics) { verse in
ForEach(verse.verseContent { line in
Text(line)
)
}
)
When I try this I get the following error
Explicitly specify the generic arguments to fix this issue
Generic parameter 'ID' could not be inferred
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'String' conform to 'Identifiable'
Solved! It just needed .self for both of the ForEach statements

Swift Array multiple appending on click

I'm creating a button that when clicked adds the current date and time to an array but when I try to append to the array it only appends once and not repeating the process
the Entries struct:
struct Enteries {
var dates:[String] = []
}
convert date to String:
func DateConverter(){
format.timeZone = .current
format.dateFormat = "yyyy-MM-dd HH:mm"
dateString = format.string(from: currentDate)
}
The function that appends: also its called later whenever an IBAction is triggered
func AddToDatabase () {
var entery = Enteries()
entery.dates.append(dateString)
print(entery.dates)
}
`
Yikes, there's a lot going on here.
First of all, Swift's convention is to use lowerCamelCase for functions. Only type names should be UpperCamelCase.
Secondly, function names should be verbs or verb phrases, type names should be nouns. If I saw DateConverter in some code, I would expect it to be a type. It's an UpperCamelCase noun, that's how types should be named. But yours is a function (which would be a total surprise to every other Swift developer, because it violates the expectations they've built up from Swift's naming conventions), that function should probably be called parseDate.
Which way does DateConverter convert? From String to Date, Date to String, or both? What's its input? What's it's output? These things should be obvious from a good function name, but are totally unknown here without looking at the implementation.
Critically, the DateConverter function doesn't take input from parameters, and doesn't return a result, instead it takes input from a side effect (accessing the variable currentDate) and returns a result via side effect (writing to an a variable dateString). This is really bad, for several reasons:
It's not reusable. You have no way to use this date parsing code somewhere else without copy/pasting it, which is how code duplication and complexity arise. If you ever decide to change the date format in your app, you won't have a central source-of-truth that you can change, instead you'll have to manually hunt down every copy of this function, and change it, hoping you don't miss any. Not good.
It's not thread safe
It's more complex than a simple function that has type (Date) -> String. It obfuscates what's going on.
It defies peoples' expectations, without justification.
Enteries.dates has a default value of [], which doesn't seem to be a good idea if you're going to be appending to it as soon as you create it. Instead, take the array via an initializer parameter.
Enteries.dates has type [String]. Why?! You already have Date objects, store those!
They're smaller (in memory)
They're presentation-agnostic, meaning you can properly format them for different interfaces and different locales at a later time, as necessary
They support date math. I often see people storing dates as strings, and ask questions like "How do I sort my array of dates?" (which are actually stored as strings), "How do I add 1 day to "2019-12-24", and they start doing funky parsing, splitting, joining, and it's all just an absolute mess
Here's how I would improve this code:
struct Entries {
var entries: [Entry]
}
struct Entry {
let date: Date
}
// Call this from your view layer, only when you're about to present a `Date` to a user.
func parse(date: Date) -> String {
let df = DateFormatter()
df.timeZone = .current
df.dateFormat = "yyyy-MM-dd HH:mm"
return format.string(from: currentDate)
}
var entries = Entries(entries: [])
func addToDatabase(entry: Entry) {
entries.append(entry)
print(enteries.entries)
}
you are creating a new entery object eveytime the function is called. SO its creating a new object everytime. Declare your entery object outside the function.
var entery = Enteries()
func AddToDatabase () {
entery.dates.append(dateString)
print(entery.dates)
}

Swift: String unable to read data

After separating fields with
var fields = row.characters.split {$0 == "\t"}.map { String($0) }
the debugger shows "unable to read data" for one field.
What does it mean, except there is no data? I want to check it, but it is no empty string ("") and it's not nil.
Are you using Swift 2 in XCode 7? Might wanna give this a try:
var fields = row.characters.split {"\t"}.map(String.init)
Or maybe your problem is that row ends with a "\t" and the final string is not valid for String init. I would suggest split and then print your results as debug.
source: Ethan's answer

How would you create a multidimensional array with n dimensions in Swift?

For instance, asume
var hierarchicalFileSystem: [[String]] = []
This allows one to support one layer of folders, but there appears to be no way to create an array in Swift like the one above but with an undefined number of nested String arrays.
Am I missing something here?
An array of arrays (of arrays of arrays...) of strings doesn't really make much sense to represent a file system.
What I'd instead recommend is making a class or struct to represent objects in the file system. Perhaps something like this:
struct FileSystemObject {
let name: String
let extension: String?
let isFolder: Bool
let contents: [FileSystemObject]?
}
Something like this let's us represent a file system quite nicely.
let fileSystem = [FileSystemObject]()
So, your fileSystem variable here is an array of FileSystemObjects and it represents the root. Each object within the root has its own set of details (its name, its file extension if it has one, and whether or not its a folder), and if it's a folder it has a non-nil contents property, and if it's a non-empty folder, that contents array of FileSystemObjects contains more file system objects (some of which are folders of course, which contain contents themselves).
What you can perhaps do is create an array with AnyObject and add new depths as you need it
var fileSystem: [AnyObject] = []
This would be a very bad way of representing a file system however and you should really go with some kind of tree structure like
struct Node {
children: [Node]?
parent: Node?
name: String
}
Swift is type-safe language. You have to declare type of your variable, or set it to AnyObject, but please don't. So, answering your question: yes it's possible:
var array: [AnyObject] = [[[1,2,3], [1,2,3]], [[1,2,3],[1,2,3]]]
But this is awful. Try to figure out better representation for your problem. Maybe custom structures.
you can have as much dimensional array as you want. is it a good idea? i don't think ...
var threeDArray: Array<Array<Array<String>>> = []
let oneDArray = ["1","2","3"]
let twoDArray1: Array<Array<String>> = [oneDArray, oneDArray, oneDArray, oneDArray, oneDArray]
let twoDArray2 = twoDArray1 + [["4","5","6"],["7","8","9"]]
threeDArray.append(twoDArray1)
threeDArray.append(twoDArray2)
let arr = [threeDArray,threeDArray,threeDArray]
print(arr.dynamicType) // Array<Array<Array<Array<String>>>>

Resources