I have a simple struct that I will build upon. Right now it has one field, an Int.
struct Card: CustomStringConvertible {
let value: Int
init(value: Int) {
self.value = value
}
var description: String {
return "\(String(value))"
}
}
If I do this, I get the Card to print it's value
let c = Card(value: 1)
print(c)
Now if I put an array of Cards in a CardController like this:
class CardController: ObservableObject {
#Published
var cards: [Card] = [
Card(value: 1),
Card(value: 2),
Card(value: 3)
]
Picker(selection: $selectedCardValue, label: Text("Choose a card")) {
ForEach(0..<cardController.cards.count) {
Text(self.cardController.cards[$0])
}
}
Text("You selected \(selectedCardValue)")
I'll get the error Initializer 'init(_:)' requires that 'Card' conform to StringProtocol. I'm not sure why I get this error. If I instead just change the cards to a type of [String] and values ["1", "2", "3"], the code works fine.
Any idea what's wrong here?
As E.Coms noted, the solution is to use one of the following:
Text(self.cardController.cards[$0].description)
Text(String(describing: self.cardController.cards[$0]))
Here's an explanation of why you have to do this inside the Text initializer, but not print().
Look at the two initializers for Text:
init(verbatim content: String) (docs)
init<S>(_ content: S) where S : StringProtocol (docs)
You must pass either a String or a Substring, the only two types conforming to StringProtocol. In this case, even though your type conforms to CustomStringConvertible, you are still passing a Card.
Contrast this with something like the print function:
func print(_ items: Any..., separator: String = " ", terminator: String = "\n") (docs)
Notice that the print function's arguments are denoted by Any, which is explained as
Any can represent an instance of any type at all, including function types.
The print function then converts whatever you passed it to a String:
The textual representation for each item is the same as that obtained by calling String(item).
String has an initializer which takes a type conforming to CustomStringConvertible and returns the description property.
So the reason you can write print(Card()) and not Text(Card() is because the print function has an intermediate step through String that can understand your conformance to CustomStringConvertible, but Text does not. If Text allowed you to pass it any type, it would be both more ambiguous ("What is the text representation of this type?" is not necessarily immediately apparent, as it depends on a hierarchical set of protocols), and more work for the SwiftUI system, which is already doing a lot.
You may miss the description by chance.
ForEach(0..<cardController.cards.count) {
Text(self.cardController.cards[$0].description)
}
Related
struct NotesView: View {
#State var notesArray = [Note]()
public var deleteid: String
var body: some View {
List{
ForEach(notesArray, id: \._id) { notesArray in
NavigationLink(destination: AddNotesView(addNotesViewIdentifier: "updateNote", id: notesArray._id, title: notesArray.title, note: notesArray.note, noteDate: notesArray.date)){
HStack {
Text(notesArray.title)
deleteid = notesArray._id //ERROR - Cannot assign to property: 'self' is immutable
}
}
}
.onDelete(perform: deleteNoteAtIndex)
}
}
func deleteNoteAtIndex(at offsets: IndexSet){ APIFunctions.functions.DeleteNote(id: _id) }
I was expecting the variable "deleteid" to update.
I assumed you can modify any variable by calling that variable and set it equal to a new value.
Like this
First declare variable:
var deleteid: String
next modify variables string valve
deleteid = notesArray._id
A couple of things:
This isn't directly related to your question, but may help you navigation your own code better... When you create a ForEach view to iterate over an array, you should use a different name for the value that represents each element in the iteration. Here, you're using the name notesArray for your array of notes, then creating a second local variable called notesArray for the loop. That variable inside the block will be an instance of Note, so I'd name it note, e.g.:
ForEach(notesArray, id: \._id) { note in
NavigationLink(destination: AddNotesView(addNotesViewIdentifier: note._id, // etc
}
If you want variables to be modifiable inside views, they should be #State variables. This is important due to the way Swift struct lifecycles work, and how the SwiftUI rendering system loads and reloads structs as it works out what has changed.
I'm not entirely sure what deleteid is supposed to represent here, and it's possible you don't need it at all. If you're using the onDelete modifier to implement SwiftUI's native swipe-to-delete system, SwiftUI will give you an IndexSet, which is a collection (usually of just one) of the positions of the item(s) to delete in the array.
From there, you can find the item(s) at each index and then either remove them, or lookup some other value (e.g., their _id attribute) and do some other operation on them.
So the method you might call in onDelete could look something like:
func deleteNoteAtIndex(offsets: IndexSet) {
// get the array objects that the offsets point to
let notes = offsets.map { noteArray[$0] }
for note in notes {
APIFunctions.functions.deleteNote(id: note._id)
}
}
I'm following a Kodeco tutorial(https://www.kodeco.com/4161005-mvvm-with-combine-tutorial-for-ios) for SwiftUI and Combine which hits an API and shows the data in a list. The tutorial doesn't explain how I can navigate to a detail view from the list, and I would like to implement this, but I'm having trouble adapting my code to do this.
My List is set up like this:
var body: some View {
NavigationView {
List {
characterSection
}
.navigationTitle("Characters")
}
}
Character Section:
var characterSection: some View {
Section {
ForEach(viewModel.dataSource, content: CharacterListRowView.init(viewModel:))
}
}
I would like to be able to do something like the following to navigation to a detail screen, but I'm getting errors:
ForEach(viewModel.dataSource, content: CharacterListRowView.init(viewModel:)) { item in
NavigationLink(destination: viewModel.characterDetailView) {
}
}
This obviously does not work but hopefully you can see what I am trying to do. I'm receiving these errors:
Generic parameter 'ID' could not be inferred,
Incorrect argument label in call (have ':content::', expected '_:id:content:')
Trailing closure passed to parameter of type 'KeyPath<Array.Element, ID>' that does not accept a closure
Here is my row view model in case the issue is with this, could be something to do with the ID?:
import Foundation
import SwiftUI
struct CharacterRowViewModel: Identifiable {
private let item: CharacterListResponse
var id: Int {
return item.char_id
}
var title: String {
guard let title = item.name?.description else { return "" }
return title
}
init(item: CharacterListResponse) {
self.item = item
}
}
Thanks for any help, I am new to SwiftUI.
One of your biggest problems is that your ForEach view is defining its contents twice, and the compiler (quite rightly) doesn't understand.
In the form you're using, ForEach takes two arguments:
the items to loop through
what view to generate for each item in the loop
That second argument is called content and it expects to be given something that receives a single item, and returns the View to use for that item.
When you use the form
ForEach(viewModel.dataSource, content: CharacterListRowView.init(viewModel:))
you're passing in a function – in this case CharacterListRowView.init - that receives a row view model, and returns a CharacterListRowView.
That's convenient when the init method matches the expected function type. But the more common form is the type you're trying to switch to - a block that receives an item, and inside which you build the view for the item. When we use that form, we usually use Swift's trailing block syntax. This allows us to omit the argument name. So when we say
ForEach(items) { item in
// view code here
}
it's the same as
ForEach(items, content: { item in
// view code here
})
Now, look again at the code you've amended:
ForEach(viewModel.dataSource, content: CharacterListRowView.init(viewModel:)) { item in
NavigationLink(destination: viewModel.characterDetailView) {
}
}
Here, you're not replacing the first form of content: with a block form, you're attempting to provide a second content block. The compiler is having a meltdown because you're providing two items to something that only expects one. When this happens, sometimes the compiler's error messages can get very confusing -- it reports how what it's tried to do to make your code work hasn't worked, rather than the direct cause of the failure.
So in your ForEach loop, remove the explicit content::
ForEach(viewModel.dataSource) { item in
NavigationLink(destination: viewModel.characterDetailView) {
}
}
You may still have problems after this step, but hopefully any error messages will be a little bit easier to understand.
I have an ObservableObject with a published dictionary of strings to arrays with arrays with Ints:
class MyObservableObject: ObservableObject {
#Published var myDict: [String: [[Int]]]
}
And I want to pass one array of Ints as a Binding from inside the same class to a function of a different struct:
{
...
func someFunc(key: String, index: Int) {
someStruct.func(myDict[key]![index])
}
...
}
I understand that #Published vars can't be passed as Bindings. I'm still hoping that there's any way to achieve this. I also tried storing a reference to the array in the other struct using an inout variable, also without success.
#Published vars can't be passed as Bindings
It is not true - it is possible, via projected value using '$', but you want to pass not a property, but part of value of a property, and this is different thing.
The context is not clear and this someFunc smells not-well :) - I'd say it is needed some refactoring here, but, anyway, technically it is possible to do what you want using dynamically in-place generated binding, like
func someFunc(key: String, index: Int) {
guard myDict[key] != nil else { return }
someStruct.func(Binding<[Int]>(
get: { self.myDict[key]![index] },
set: { self.myDict[key]![index] = $0 }
))
}
I'm having trouble printing localized text conditionally. For example, this localizes properly:
if valueFromDb.isEmpty {
Text("value_is_empty") //localized text
} else {
Text(valueFromDb)
}
It prints some text in the user's language if valueFromDb is empty, or it prints valueFromDb as it is if it's not. However, when I try to use the ternary operator it doesn't work:
Text(valueFromDb.isEmpty ? "value_is_empty" : valueFromDb)
When valueFromDb is empty, it prints "value_is_empty" rather than actual localized text. I get an error (a random one higher up in the hierarchy thanks to SwiftUI) when trying to cast it as LocalizedStringKey.
Edit: To be clear, I know I can do this:
valueFromDb.isEmpty ? Text("value_is_empty") : Text(valueFromDb)
However, I want to put the ternary conditional inside the Text() brackets because I will do this for several views, and each one will have many modifiers, so the code will become quite bloated.
The problem is due to type inference. You have to declare myString to be of type LocalizedStringKey and then everything will work as expected.
When you declare:
#State var mySrtring: LocalizedStringKey = "whatever"
Then:
Text(myString.isEmpty ? "error_text_localized" : myString)
uses this initializer:
public init(_ key: LocalizedStringKey,
tableName: String? = nil,
bundle: Bundle? = nil,
comment: StaticString? = nil)
When you declare it like this:
#State var mySrtring: String = "whatever"
Then:
Text(myString.isEmpty ? "error_text_localized" : myString)
uses this initialiser:
public init(verbatim content: String)
You have to put your valueFromDb in quotations, then it should work fine.
Text(valueFromDb.isEmpty ? "value_is_empty" : "\(valueFromDb)")
I have a function:
func parseJSON3(inputData: NSData) -> NSArray {
var tempDict: (id:Int, ccomments:Int, post_date:String, post_title:String, url:String) = (id: 0, ccomments: 0, post_date: "null", post_title: "null", url: "null")
var resultArray: (id:Int, ccomments:Int, post_date:String, post_title:String, url:String)[] = []
var error: NSError?
var jsonDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(inputData, options: NSJSONReadingOptions.MutableContainers, error: &error) as NSDictionary
var firstArray = jsonDictionary.objectForKey("locations") as NSArray
for dict in firstArray {
tempDict.id = dict.valueForKey("ID") as Int
tempDict.ccomments = dict.valueForKey("ccomments") as Int
tempDict.post_date = dict.valueForKey("post_date") as String
tempDict.post_title = dict.valueForKey("post_title") as String
tempDict.url = dict.valueForKey("url") as String
resultArray.append(tempDict)
}
return resultArray
}
In line
resultArray.append(tempDict)
I have an error:
Missing argument for parameter 'ccomments' in call
Why? Help please....
It looks to me like resultArray.append() is treating the tuple a little bit like a variadic parameter, and trying to expand the tuple to match its own arguments. It's complaining about your second parameter because it's only expecting one. I haven't seen this behavior for Array.append() documented anywhere, so I would say it's a bug in Swift.
Using the appending operator += doesn't seem to have that issue:
resultArray += tempDict
So this is pretty wild - not sure if I would qualify it as a bug or as undocumented behavior, but it's definitely something that should be on the radar for a fix / clarification!
The situation is that append is treating your argument tempDict (which we would expect to be the only argument to an Array method that takes a single member and adds it to the collection) as the first argument in a signature where it is looking for 5 arguments (!), one for each member of the Tuple type that the Array holds.
See the following for some interesting behavior (including assigning a label to the single member of a 1-member 'Tuple' ??) ->
var arrayOne: Array<String> = []
arrayOne.append("hi")
println(arrayOne[0]) // hi
var arrayTwo: Array<(String)> = [] // where (String) is a single-member Tuple
arrayTwo.append("hi")
println(arrayTwo[0]) // hi
println(arrayTwo[0].0) // hi -> using .0 subscript to access the first member of the Tuple
// wanna see something crazy? remember arrayOne, that holds members of type String?
println(arrayOne[0].0) // hi -> this Array does not hold Tuples, but it looks like we can still treat its members like "single-member Tuples"?
var arrayThree: Array<(str: String)> = [] // members of the Array are single-member Tuples with the label 'str' for their member
arrayThree.append(str: "hi") // now we can't use append without providing the label 'str', acting as what looks like an argument label?
var byeString = "bye"
var byeTuple = ("bye")
arrayThree += byeString // += doesn't care about the label, and will take either a String or a single-member Tuple holding a String
arrayThree += byeTuple
println(arrayThree[0]) // hi
println(arrayThree[0].0) // hi
println(arrayThree[0].str) // hi -> accessing the single member of the Tuple by its label
...so in your case, where you are seeing the error with append what it wants you to do is (using the labels you used to declare the Tuple as something that looks like argument labels):
var resultArray: (id:Int, ccomments:Int, post_date:String, post_title:String, url:String)[] = []
...
resultArray.append(id: someIntValue, ccomments: someOtherIntValue, post_date: someStringValue, post_title: someOtherStringValue, url: someAnotherStringValue)
...and of course, as discussed, you can avoid doing that by just using += instead
Crazy stuff! could be by design to serve some purpose, could be a consequence of protocol inheritance that wasn't meant to have this effect... would be interesting to know the answer!
resultArray.append() seems to be taking tempDict as the first tuple element (id).
Changing it to :
resultArray += tempDict
seems to compile and work.
I'm not sure why append() doesn't behave the same way, maybe you can file a bug!