I don't fully understand what exactly \.self does in the following code:
struct ContentView: View {
#State private var numbers = [Int]()
#State private var currentNumber = 1
var body: some View {
VStack {
List {
ForEach(numbers, id: \.self) {
Text("\($0)")
}
}
Button("Add Number") {
self.numbers.append(self.currentNumber)
self.currentNumber += 1
}
}
}
}
I'm following this: https://www.hackingwithswift.com/books/ios-swiftui/deleting-items-using-ondelete.
I have a really basic understanding of the language right now, so I may not understand high level explanations, so would prefer very simple explanations or analogies with verbose descriptions. I think it is setting the idfor each list item as each item in the numbers array? Correct me if wrong - but is each id being set as whatever Int is in each entry of the numbers array? If so, then what does \ actually do when typing \.self and what does .self actually do in combination with \?
. key paths. ForEach needs every object unique. give them unique ids with id:.self. if your objects are identifiable you dont need .self.
i wrote about this in medim if you want you can check out
id: .self tells Swift to use as unique id (keypath) the hash of the object. It explains why the name "self" is used. id: .self is especially useful for the basic Swift types like Integer and String. On the one the hand the developer cannot add to them an unique id. On the other hand all they are hashable so we can use id: .self. id: .self is useful not only for ForEach, but for List also.
Related
See the following app screens:
Content View Screen:
Content View with hierarchical list children rows disclosed:
Parent Row Detail View:
Child Row Detail View:
Referencing the above views, here are the steps I do and the resulting problem I’m trying to solve:
Launch the app.
From the Functions (Content View) presented at launch, see that there is one item listed in a list view (1.0 Move Vessel)
Click the yellow (my app accent color) disclosure arrow at the right of the list item.
Two subordinate child list rows appear under the parent list item, 1.1 Move Position and 1.2 Hold Position.
When I tap the parent item (1.0 Move Vessel) in the hierarchy list, I'm successfully able to navigate to a detail view for that tapped item.
Edit the description of the 1.0 Move Vessel item (defaults to test) of the tapped item properties in the detail view using a TextEditor view.
Click yellow Save button at top left of detail view. The app navigates back to the parent Functions (Content View).
Click on the parent 1.0 Move Vessel row again.
See that description was successfully saved and now displayed from the change made in Step 5 and 6.
Repeat steps 5 through 8 again for 1.1 Move Position list row.
See that the edit/change made to the description was not saved and the default test1 description is displayed instead (not what is wanted).
Repeat steps 5 through 8 again for 1.2 Hold Position list row.
See that the edit/change made to the description was not saved and the default test2 description is displayed instead (not what is wanted).
I think I may have a problem in my save code logic and I'm trying to investigate.
Here are the swift files for the Detail View, the View Model, and the Model (I’ve not included the content view code because that code is working ok with the detail view. Again, I think the problem is in my save button and function call code for updating the view model.
NOTE: sorry that I can’t seem to figure out how to get all the code for a file contiguous in the code view. I seem to have some closing braces that don’t appear in the code view. I think you can still follow the code.
struct FunctionDetailView: View {
#State var vesselFunction: VesselFunction
#State var vesselFunctionDescription: String
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var functionViewModel : FunctionViewModel
var body: some View {
NavigationView {
Form {
Text("Enter description below")
TextEditor(text: $vesselFunctionDescription)
.frame(height: 200)
.toolbar {
Button {
//print(vesselFunction)
vesselFunction.funcDescription = vesselFunctionDescription
//print(vesselFunction)
functionViewModel.updateVesselFunction(vesselFunction: vesselFunction)
//print(vesselFunction)
presentationMode.wrappedValue.dismiss()
} label: {
Text("Save")
}
}
}
.padding()
.navigationTitle(vesselFunction.name)
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct FunctionDetailView_Previews: PreviewProvider {
static var previews: some View {
FunctionDetailView(vesselFunction: VesselFunction(id: UUID(), name: "x.x Verb Noun", funcDescription: "Description", children: nil), vesselFunctionDescription: "placeholder")
.environmentObject(FunctionViewModel())
.preferredColorScheme(.dark)
}
}
FunctionViewModel.swift
#MainActor class FunctionViewModel: ObservableObject {
#Published private(set) var decomp : [VesselFunction] = [
VesselFunction(id: UUID(), name: "1.0 Move Vessel", funcDescription: "test", children: [
VesselFunction(id: UUID(), name: "1.1 Move Position", funcDescription: "test1", children: nil),
VesselFunction(id: UUID(), name: "1.2 Hold Position", funcDescription: "test2", children: nil)
])
]
func updateVesselFunction(vesselFunction: VesselFunction) {
/*
if let index = decomp.firstIndex(where: { (existingVesselFunction) -> Bool in
return existingVesselFunction.id == vesselFunction.id
}) {
//run this code
}
*/
// cleaner version of above
if let index = decomp.firstIndex(where: { $0.id == vesselFunction.id }) {
decomp[index] = vesselFunction.updateCompletion()
}
/*
else {
for item in decomp {
if item.children != nil {
if let index = item.children?.firstIndex(where: { $0.id == vesselFunction.id }) {
item.children![index] = vesselFunction.updateCompletion()
}
}
}
} */
}
}
FunctionModel.swift
struct VesselFunction: Identifiable {
let id : UUID
let name : String
var funcDescription : String
var children : [VesselFunction]?
init(id: UUID, name: String, funcDescription: String, children: [VesselFunction]?) {
self.id = id
self.name = name
self.funcDescription = funcDescription
self.children = children
}
func updateCompletion() -> VesselFunction {
return VesselFunction(id: id, name: name, funcDescription: funcDescription, children: children)
}
}
As you can see from the else and for-in loop code commented out at the bottom of the FunctionViewModel code, I was trying to see if I needed to do something like this code to access the children VesselFunction array entries of the decomp published property. With the if let index code that is not commented out, the save function works but only for the top-level decomp array VesselFunction elements, not the nested children arrays elements.
Any help would be appreciated so all decomp array elements, both parent and nested children, can be updated when the TextEditor field is changed and the Save button is pressed in the FunctionDetailView.
NOTE: I am only showing a 1 level deep nested array of children for the decomp property. I actually want to have multiple (at least 3) level of children arrays, so if you have any ideas how to make an updateVesselFunction function work for multiple children array elements, I would appreciate it.
In the main View use ForEach($model.items) { $item in so you get a write access to the model item. In the detail View change the #State to #Binding.
The issue isn't so much your code right now, as it is the architecture of the program. You really need to reorganize the app with MVVM concepts in mind. If you are not sure of them, study Apple’s SwiftUI Tutorials & Stanford’s CS193P. Without a proper architecture, you have gotten lost down a rabbit hole to the extent that I gave up trying to fix the code.
Also, given the structure of your data, I would give serious consideration to using Core Data to model it. Your VesselFunction struct contains an array of VesselFunction, and that it much better modeled as a relationship, rather than having a struct hold an array of the same struct which can hold an array of the same struct. It is a nightmare to deal with as a struct, instead of as a Core Data class.
I would also consider make your FunctionDetailView just display data, and have a separate editing view. This will keep your view separate and easier to manage.
Lastly, you have a lot of redundancy in your naming conventions. Theoretically, you could be trying to access a piece of data at functionViewModel.funcDescription (Not to mention: functionViewModel.children[index].children[subIndex].children[subSubIndex].funcDescription); this can get a bit unwieldy. The further you go down, the worse it will get.
Is it possible to declare a View as a var in some way, as in below struct?
struct OptionViews {
var title: String
var imageName: String
var targetView: View
}
I want to use the above struct to present as an array of possible selections (as in a settings view) where if I click on an item a it should open the targetView. However, the above struct is not allowed due to "Protocol View can only be used as a generic constraint", is there a way around this or another way to accomplish this? I need to know which View should be opened when the specific item is selected, but if i cant specify the view as part of the item, that does not seem possible.
Thanks,
Marcus
I guess what #Asperi said in comment is right. Not sure what you exactly want but maybe you could make your struct generic like:
struct OptionViews<Content> where Content: View {
var title: String
var imageName: String
var targetView: Content
}
// example
let optionViews = OptionViews(title: "Titel",
imageName: "Image",
targetView: Image(systemName: "heart"))
I have list of structs that I display in the view using a ForEach, I am trying to add to the list and when the user taps they see information for that item. Using two approaches I get two different results.
First Approach
The view updates the view when items are added perfectly, but changing the note in the Details changes values of all notes.
#Binding var person: Person
ForEach(self.person.notes) {
note in
DetailsCard(person: self.$person, note: notes)
}
Second Approach
The view does not update when notes are added, only when the view reappears does it show the new items. But I when the items are shown, the details view works as expected.
#Binding var person: Person
ForEach(self.person.notes.indices) { index in
VStack {
DetailsCard(person: self.$person, note: self.person.notes[index])
}
}
DetailView
#Binding var person: Person
#State var note: Note
This should be a fairly simple task but working with SwiftUI I am confused by the two different results for similar approaches. Does anyone have any ideas on why this is occurring and how to correctly dynamically update the view and pass the values.
Try using dynamic ForEach loops, ie. with an explicit id parameter:
ForEach(self.person.notes, id:\.self)
ForEach(self.person.notes.indices, id:\.self)
Note: id must conform to Hashable. If you decide to use self as an id, it will only work if your item conforms to Hashable. Otherwise you may want to use some property (or just add the conformance).
I'm looking at an example of using SwiftUI with Combine: MVVM with Combine Tutorial for iOS at raywenderlich.com. A ViewModel implementation is given like this:
class WeeklyWeatherViewModel: ObservableObject, Identifiable {
// 2
#Published var city: String = ""
// 3
#Published var dataSource: [DailyWeatherRowViewModel] = []
private let weatherFetcher: WeatherFetchable
// 4
private var disposables = Set<AnyCancellable>()
init(weatherFetcher: WeatherFetchable) {
self.weatherFetcher = weatherFetcher
}
}
So, this makes some sense to me. In a view observing the model, an instance of the ViewModel is declared as an ObservedObject like this:
#ObservedObject var viewModel: WeeklyWeatherViewModel
And then it's possible to make use of the #Published properties in the model in the body definition of the View like this:
TextField("e.g. Cupertino", text: $viewModel.city)
In WeeklyWeatherViewModel Combine is used to take the city text, make a request on it, and turn this in to [DailyWeatherRowViewModel]. Up to here, everything is rosey and makes sense.
Where I become confused is that quite a lot of code is then used to:
Trigger a fetch when city is changed.
Keep hold of the AnyCancellable that's looking up weather data.
Copy the output of the weather look up in to dataSource by a sink on the weather fetch Publisher`
It looks like this:
// More in WeeklyWeatherViewModel
init(
weatherFetcher: WeatherFetchable,
scheduler: DispatchQueue = DispatchQueue(label: "WeatherViewModel")
) {
self.weatherFetcher = weatherFetcher
_ = $city
.dropFirst(1)
.debounce(for: .seconds(0.5), scheduler: scheduler)
.sink(receiveValue: fetchWeather(forCity:))
}
func fetchWeather(forCity city: String) {
weatherFetcher.weeklyWeatherForecast(forCity: city)
.map { response in
response.list.map(DailyWeatherRowViewModel.init)
}
.map(Array.removeDuplicates)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] value in
guard let self = self else { return }
switch value {
case .failure:
self.dataSource = []
case .finished:
break
}
},
receiveValue: { [weak self] forecast in
guard let self = self else { return }
self.dataSource = forecast
})
.store(in: &disposables)
}
If I look in Combine for the definition of the #Published propertyWrapper, it seems like all does is provide projectedValue which is a Publisher, which makes it seem like it ought to be possible for WeeklyWeatherViewModel to simply provide the Publisher fetching weather data and for the view to make use of this directly. I don't see why the copying in to a dataSource is necessary.
Basically, what I'm expecting is there to be a way for SwiftUI to directly make use of a Publisher, and for me to be able to put that publisher externally from a View implementation so that I can inject it. But I've no idea what it is.
If this doesn't seem to make any sense, that figures, as I'm confused. Please let me know and I'll see if I can refine my explanation. Thanks!
I don't have a definitive answer to this and I didn't find a magic way to have SwiftUI make use of a Publisher directly – it's entirely possible that there is one that eludes me!
I have found a reasonably compact and flexible approach to achieving the desired result, though. It cut down the use of sink to a single occurrence that attaches to the input (#Published city in the original code), which substantially simplifies the cancelation work.
Here's a fairly generic model that has an #Published input attribute and a #Published output attribute (for which setting is private). It takes a transform as input, and this is used to transform the input publisher, and is then sinked in to the output publisher. The Cancelable of the sink is stored.
final class ObservablePublisher<Input, Output>: ObservableObject, Identifiable {
init(
initialInput: Input,
initialOutput: Output,
publisherTransform: #escaping (AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never>)
{
input = initialInput
output = initialOutput
sinkCancelable =
publisherTransform($input.eraseToAnyPublisher())
.receive(on: DispatchQueue.main)
.sink(receiveValue: { self.output = $0 })
}
#Published var input: Input
#Published private(set) var output: Output
private var sinkCancelable: AnyCancellable? = nil
}
If you wanted a substantially less generic kind of model, you can see it's pretty easy to set up having the input (which is a publisher) be filtered in to the output.
In a view, you might declare an instance of the model and use it like this:
struct SimpleView: View {
#ObservedObject var model: ObservablePublisher<String, String>
var body: some View {
List {
Section {
// Here's the input to the model taken froma text field.
TextField("Give me some input", text: $model.input)
}
Section {
// Here's the models output which the model is getting from a passed Publisher.
Text(model.output)
}
}
.listStyle(GroupedListStyle())
}
}
And here's some silly setup of the view and its model taken from a "SceneDelegate.swift". The model just delays whatever is typed in for a bit.
let model = ObservablePublisher(initialInput: "Moo moo", initialOutput: []) { textPublisher in
return textPublisher
.delay(for: 1, scheduler: DispatchQueue.global())
.eraseToAnyPublisher()
}
let rootView = NavigationView {
AlbumSearchView(model: model)
}
I made the model generic on the Input and Output. I don't know if this will actually be useful in practice, but it seems like it might be.
I'm really new to this, and there might be some terrible flaws in this such as inefficiencies, memory leaks or retain cycles, race conditions, etc. I've not found them yet, though.
You can use URLSessionDataTaskPublisher and refactor out networking from all view models.
If you feel some part of the tutorial seems redundant, that is because it is.
MVVM in such usage is redundant and does not do the job better.
I have a refactored version (networking refactored, all view models removed) of that tutorial if you are interested in details.
Doing a ForEach within another ForEach in a SwiftUI View produces unexpected results - almost like they are stepping on each other's counters. Not clear as to what's happening. I need to display multi branched arrays and have tried a number of variants but keep running into the same problem
I have a few projects where this has come up. Tries using ranges (0..
Is this just a beta issue or am I missing something? I've included an example project that demonstrated the problem.
using XCode 11 Beta (11M392r)
Thanks!
import SwiftUI
struct ContentView: View {
let topTier:[String] = ["Apple", "Banana", "Cherry"]
let nextTier:[String] = ["Abalone", "Brie", "Cheddar"]
var body: some View {
List {
ForEach (topTier.indices, id: \.self) { a in
Text(self.topTier[a])
ForEach (self.nextTier.indices, id: \.self) { b in
Text(self.nextTier[b]).padding(.leading, 20)
}
}
}
}
}
throws "Ambiguous reference to member 'indices'" on the fitst ForEach.
If the inner ForEach is commented it works displaying the fruits
If the outter ForEach is commented it works displaying the cheeses
I want it to display:
Apple
Abalone
Brie
Cheddar
Banana
Abalone
Brie
Cheddar
Cherry
Abalone
Brie
Cheddar
As with many of the errors that come out of the new #ViewBuilder syntax with SwiftUI during this beta cycle, the “Ambiguous reference…” message is a red herring. Your issue is that the ForEach closure expects a single view, not a view builder.
You can get your view working by simply wrapping the body of the ForEach closure in a Group, like so:
import SwiftUI
struct ContentView: View {
let topTier:[String] = ["Apple", "Banana", "Cherry"]
let nextTier:[String] = ["Abalone", "Brie", "Cheddar"]
var body: some View {
List {
ForEach (topTier.indices, id: \.self) { a in
Group {
Text(self.topTier[a])
ForEach (self.nextTier.indices, id: \.self) { b in
Text(self.nextTier[b]).padding(.leading, 20)
}
}
}
}
}
}
And voilà:
Not sure I completely understand your problem, but I had a similar, potentially related issue.
SwiftUI Nested ForEach causes unexpected ordering
I received an answer that specified that each of the cells need a different identifier, which could also be your problem.
Each time the nested ForEach is executed, the id's that are generated are the same, which results in there being lots of cells that have the same ID, and thus the unexpected reorderings etc. appear. In the linked post you can see how I solved this issue.
Summary: Each cell needs a unique ID, as you can see in this screenshot where each of the cells from section 7 and 8 have different ids.