The screen representing user profile. I want to move to the profile of chosen friend (from 'friends' list). I suppose that I have to match between "id"s. This is how I've tried, but it doesn't work. How can I match "id"s?
// This didn't work
user: session.users.first(where: { $0.id == friend.id })
.
struct DetailView: View {
var user: User
var session: Session
...
...
var body: some View {
...
// ... user profile ...
// Friends section
ForEach(user.friends, id: \.self.id) { friend in
NavigationLink(destination:
DetailView(user: session.users.first(where: { $0.id == friend.id }),
session: session)) {
HStack {
Text(friend.name)
}
}
}
}
}
this is the data organization.
class Session: ObservableObject {
#Published var users = [User]()
}
struct User: Codable {
var id: String
var isActive: Bool
var name: String
var age: Int16
var company, email, address, about: String
var registered: Date
var tags: [String]
var friends: [Friend]
}
struct Friend: Codable {
var id: String
var name: String
}
EDIT
If there is a better way to match between "id"s. How social networks do this?
// added exclamation mark (!) and it worked out
session.users.first(where: { $0.id == friend.id })!
I don't know why you do this but your Details view does not have an optional User type and you have passed an optional user (Array.first gives you an optional object). So you need to handle this or add an exlemetion sign at the end. like this
session.users.first(where: { $0.id == friend.id })!
Related
Hello everyone and happy new year,
I had learned on this site to create dictionaries to prepare sections of lists from a property of an object, for example:
struct Lieu: Identifiable, Hashable {
var id = UUID().uuidString
var nom: String
var dateAjout = Date()
var img: String
var tag: String
var tag2: String
var tag3: String
}
Preparation of the dictionary with "tag" as key:
private var lieuxParTag: [String: [Lieu]] {
Dictionary(grouping: listeDeLieux) { lieu in
lieu.tag
}
}
private var lieuxTags: [String] {
lieuxParTag.keys.sorted(by: <)
}
Then in the view, it was quite simple:
ForEach (lieuxTags, id: \ .self) {tag in
Section (header: Text (tag)) {
ScrollView (.horizontal, showsIndicators: true) {
LazyHStack {
ForEach (lieuxParTag [tag]!) {Place in
Text (place.name)
}
}
}
}
But how to make sections if "Lieu" contains tag property like this:
var tags: [String]
And in this table I integrate all the tags of a "Lieu" for example :
Lieu(nom: Paris, tags: ["tour eiffel", "bouchons"])
Thanks for your help.
Here is a working example with comments. I also tightened your code up a bit.
struct SectionsFromArrayView: View {
#State private var listeDeLieux: [Lieu] = Lieu.previewData
private var lieuxParTag: [String: [Lieu]] {
// This first loop extracts the different tags from the array into a set
// to unique them so we only iterate each tag once in the next step.
var tagSet: Set<String> = []
for lieu in listeDeLieux {
for tag in lieu.tags {
tagSet.insert(tag)
}
}
// This loop puts together the dictionary by filtering listeDeLieux by tag
var parTag: [String: [Lieu]] = [:]
for tag in Array(tagSet).sorted() {
let lieuArray = listeDeLieux.filter( { $0.tags.contains(tag) })
parTag[tag] = lieuArray
}
return parTag
}
var body: some View {
List {
// Instead of having a separate computed variable lieuxTags, you can sort the
// array of the lieuxParTag.keys
ForEach(lieuxParTag.keys.sorted(by: <), id: \ .self) {tag in
Section(header: Text (tag)) {
ScrollView(.horizontal, showsIndicators: true) {
LazyHStack {
ForEach(lieuxParTag[tag]!) { lieu in
Text (lieu.nom)
}
}
}
}
}
}
}
}
struct Lieu: Identifiable, Hashable {
var id = UUID().uuidString
var nom: String
var dateAjout = Date()
var img: String
// By making tags a set, they are uniqued
var tags: Set<String>
static var previewData: [Lieu] = [
Lieu(nom: "Alfred", img: "pencil", tags: ["pencil", "doc"]),
Lieu(nom: "Ben", img: "pencil", tags: ["pencil"]),
Lieu(nom: "Charles", img: "paperplane", tags: ["paperplane", "doc"]),
Lieu(nom: "Alfred", img: "paperplane", tags: ["paperplane"]),
Lieu(nom: "Alfred", img: "pencil", tags: ["pencil", "doc"])
]
}
If you can make your tags an enum, you can condense the code even further as you already have your limited set. This shrinks lieuxParTag further:
struct SectionsFromArrayView: View {
#State private var listeDeLieux: [Lieu] = Lieu.previewData
private var lieuxParTag: [Tag: [Lieu]] {
var parTag: [Tag: [Lieu]] = [:]
// In order to get an array, Tag.allCases, Tag must conform to CaseIterable
for tag in Tag.allCases {
let lieuArray = listeDeLieux.filter( { $0.tags.contains(tag) })
parTag[tag] = lieuArray
}
return parTag
}
var body: some View {
List {
// In order to sort the keys, Tag must conform to Comparable
ForEach (lieuxParTag.keys.sorted(), id: \ .self) {tag in
Section(header: Text(tag.rawValue)) {
ScrollView(.horizontal, showsIndicators: true) {
LazyHStack {
ForEach(lieuxParTag[tag]!) { lieu in
Text(lieu.nom) + Text(Image(systemName: lieu.img))
}
}
}
}
}
}
}
}
struct Lieu: Identifiable, Hashable {
var id = UUID().uuidString
var nom: String
var dateAjout = Date()
var img: String
// By making tags a set, they are uniqued
var tags: Set<Tag>
static var previewData: [Lieu] = [
Lieu(nom: "Alfred", img: Tag.pencil.rawValue, tags: [.pencil, .doc]),
Lieu(nom: "Ben", img: Tag.pencil.rawValue, tags: [.pencil]),
Lieu(nom: "Charles", img: Tag.paperplane.rawValue, tags: [.paperplane, .doc]),
Lieu(nom: "Alfred", img: Tag.paperplane.rawValue, tags: [.paperplane]),
Lieu(nom: "Alfred", img: Tag.pencil.rawValue, tags: [.pencil, .doc])
]
}
enum Tag: String, CaseIterable, Comparable {
case doc, paperplane, pencil
static func < (lhs: Tag, rhs: Tag) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
There is no ready-made solution for that in the standard library, but you can easily implement that yourself. You loop over your objects and for each object you loop over its tags and add the current object to the dictionary for the current tag:
var dictionary: [String: [Place]] = [:]
for place in places {
for tag in place.tags {
dictionary[tag, default: []].append(place)
}
}
I have a DataModel with the following struct:
struct Task: Codable, Hashable, Identifiable {
var id = UUID()
var title: String
var completed = false
var priority = 2
}
In one my views, I populate a list with buttons based on each element of that DataModel, essentially a Task.
I pass each task for the view like this:
ForEach(taskdata.filteredTasks(byPriority: byPriotity), id: \.id) { task in
TaskView(task: task)
}
On the view, I add each button as follow:
struct TaskView: View {
#EnvironmentObject private var taskdata: DataModel
#State var task: Task
var body: some View {
Button(action: {
task.completed.toggle() }) {
....
}
}
}
Now, when the user clicks on the button, if completed was false, it makes it true. Simple. However, this doesn't update the DataModel, so I added the following to see if I could find the item in question and update it:
struct TaskView: View {
#EnvironmentObject private var taskdata: DataModel
#State var task: Task
#State var id: UUID // added this so I can also pass the ID and try to find it
var body: some View {
Button(action: {
task.completed.toggle()
if task.completed { // if it's completed, find it an update it.
//taskdata.tasks.first(where: { $0.id == id })?.completed = true
//taskdata.tasks.filter {$0.id == id}.first?.completed = true
/*if let index = taskdata.tasks.first(where: {$0.id == id}) {
print(index)
taskdata.tasks ????
}*/
}
}) {
....
}
All the commented code is what I have tried... I get different errors, from Cannot assign to property: function call returns immutable value to cannot assign to property: 'first' is a get-only property and a few other colorful Xcode errors.
How do I find the correct object on my DataModel and correctly update its .completed to true (or false if it's clicked again).
Thank you
You need to use Array >>> firstIndex
if let index = taskdata.tasks.firstIndex(where: {$0.id == id}) {
taskdata.tasks[index].completed = true
}
There is another way for your problem using a function in extension like this custom update function:
extension Array {
mutating func update(where condition: (Element) -> Bool, with newElement: (Element) -> Element) {
if let unwrappedIndex: Int = self.firstIndex(where: { value in condition(value) }) {
self[unwrappedIndex] = newElement(self[unwrappedIndex])
}
}
}
use case:
taskdata.tasks.update(where: { element in element.id == id }, with: { element in
var newElement: Task = element
newElement.completed = true
return newElement
})
I need to display arrays of different structs, conforming to a common Protocol, in a View.
As advised in SwiftUI - View showing Elements conforming to a Protocol and ForEach over them I tried like this - it works fine!
Now I need to check elements of the array for Equality.
Letting the Protocol conform to Equatable does not compile -
It gets the error: Protocol 'Suggest' can only be used as a generic constraint because it has Self or associated type requirements.
//protocol Suggest :Equatable {
protocol Suggest {
var desc: String { get }
}
struct Person : Suggest {
var surname : String
var name: String
var id: String { return name }
var desc: String { return name }
}
struct Book : Suggest {
var titel : String
var abstact : String
var id: String { return titel }
var desc: String { return titel }
}
let books = [ Book(titel: "book 1", abstact: "abstract1"),
Book(titel: "book 2", abstact: "abstract2")
]
let persons = [ Person(surname: "John", name: "Doe"),
Person(surname: "Susan", name: "Smith"),
Person(surname: "Frank", name: "Miller")
]
struct ContentView: View {
var body: some View {
VStack {
SuggestList(list: books)
SuggestList(list: persons)
}
}
}
struct SuggestList: View {
var list : [Suggest]
// this line does not compile, if Suggest conforms to Equitable
// "Protocol 'Suggest' can only be used as a generic constraint because it has Self or associated type requirements"
var body: some View {
List(list.indices, id: \.self) { index in
Text(list[index].desc)
// .onTapGesture {
// if list.contains(list[index]){print ("hello")}
// }
}
}
}
you need use <SuggestType: Suggest> and also make Suggest protocol Equatable and then use and define == in Person and Book
struct ContentView: View {
var body: some View {
VStack {
SuggestList(list: books)
SuggestList(list: persons)
}
}
}
protocol Suggest: Equatable {
var desc: String { get }
}
struct Person: Suggest {
var surname: String
var name: String
var id: String { return name }
var desc: String { return name }
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name
}
}
struct Book: Suggest {
var titel: String
var abstact: String
var id: String { return titel }
var desc: String { return titel }
static func == (lhs: Book, rhs: Book) -> Bool {
return lhs.titel == rhs.titel
}
}
let persons = [Person(surname: "John", name: "Doe"),
Person(surname: "Susan", name: "Smith"),
Person(surname: "Frank", name: "Miller")]
let books = [Book(titel: "book 1", abstact: "abstract1"),
Book(titel: "book 2", abstact: "abstract2")]
struct SuggestList<SuggestType: Suggest>: View {
var list : [SuggestType]
var body: some View {
List(list.indices, id: \.self) { index in
Text(list[index].desc)
.onTapGesture {
if list.contains(list[index]){ print(list[index].desc) }
}
}
}
}
This Answer is belong to question in comments part! About Equatable function.
If you do not define Equatable function explicitly, then Xcode would take care itself if it can infer it by itself, some times in complex struct it will ask you to show it when instance of your struct are equal, but when you define Equatable function explicitly Xcode would apply your custom rule, for example I create 2 type of Person, which in first one PersonV1 we did not define == for it but in second one PersonV2 we did defined! So Xcode would be taking all persons with same name equal in PersonV2 if even they have deferent surname. try this down code for more real testing example. And any update for surname in PersonV2 would not take any place, because it does not count in determine if 2 instance of PersonV2 are equal or not! Once you initialized an instance of PersonV2, the surname will not be updatable anymore. you can try to update but it will not applied because in make no deference if this instance is the same or not!
Notice: We could make PersonV2 equality function to re act to surname change as well with this code, but I think you want to work just with name like in your question:
static func == (lhs: PersonV2, rhs: PersonV2) -> Bool {
return lhs.name == rhs.name && lhs.surname == rhs.surname
}
struct ContentView: View {
#State private var person1: PersonV1 = PersonV1(surname: "Frank", name: "Miller")
#State private var person2: PersonV2 = PersonV2(surname: "John", name: "Doe")
var body: some View {
VStack(spacing: 50.0) {
VStack(spacing: 20.0) {
Button("update name of person1") { person1.name += " updated!" }
Button("update surname of person1") { person1.surname += " updated!" }
}
.padding()
.background(Color.yellow)
.onChange(of: person1) { newValue in print("onChange for person1:", newValue) }
VStack(spacing: 20.0) {
Button("update name of person2") { person2.name += " updated!" }
Button("update surname of person2") { person2.surname += " updated!" }
}
.padding()
.background(Color.red)
.onChange(of: person2) { newValue in print("onChange for person2:", newValue) }
}
}
}
protocol Suggest: Equatable {
var desc: String { get }
}
struct PersonV1: Suggest {
var surname: String
var name: String
var id: String { return name }
var desc: String { return name }
}
struct PersonV2: Suggest {
var surname: String
var name: String
var id: String { return name }
var desc: String { return name }
static func == (lhs: PersonV2, rhs: PersonV2) -> Bool {
return lhs.name == rhs.name
}
}
I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...
struct Group: Identifiable, Codable {
var id = UUID()
var name: String
var number: Int
var spaces: Bool
var businesses: [Business]
}
struct Business: Identifiable, Codable {
var id = UUID()
var name: String
var address: String
var space: Int
var enabled: Bool
}
These are used in a class with an Observable var that stored in User Defaults
class GroupSettings: ObservableObject {
#Published var groups = [Group]() {
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
}
}
init() {
if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
if let userDefaultConfig = try?
PropertyListDecoder().decode(Array<Group>.self, from: configData){
groups = userDefaultConfig
}
}
}
}
Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...
I pass a Group from the first view which has the array of Groups in to the detail screen with the #Binding property ...
#Binding var group: Group
var body: some View {
TextField("", text: $group.name) <---- WORKS
List {
ForEach(self.group.businesses){ business in
if business.enabled {
Text(business.name) <---- WORKS
TextField("", business.address) <---- FAILS
TextField("", value: business.space, formatter: NumberFormatter()) <---- FAILS
} else {
Text("\(business.name) is disabled"
}
}
}
}
Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.
Thanks in advance!
You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...
List {
ForEach(group.businesses.indices) { index in
TextField("", text: $group.businesses[index].address)
}
}
An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:
struct TestView: View {
#Binding var group: Group
var body: some View {
TextField("", text: $group.name)
List {
let items = Array(zip(group.businesses.indices, group.businesses))
ForEach(items, id: \.1.id) { index, business in
if business.enabled {
Text(business.name)
TextField("", text: $group.businesses[index].address)
} else {
Text("\(business.name) is disabled")
}
}
}
}
}
Hello everyone
I am creating a form that allows me to modify the data of #EnvironmentObject variable.
Therefore, I would like to be able to create a Picker that returns a String. However, after many unsuccessful attempts, I have the impression that a Picker cannot return a String.
Anyone would have an idea for a Picker that returns a String (maybe through UIKit ?).
Here's my code :
struct UserStruct {
var firstName: String
var lastName: String
var birthDate: Int
var city: String
var postalCode: Int
var street: String
var streetCode: String
var country: String
}
class User: ObservableObject {
#Published var userProfile = UserStruct()
// Other stuff here
}
// Then in my FormView:
// I declare the object as #EnvironmentObject
#EnvironmentObject var userStore: User
// I declare an array which contains all the country for the picker
let country = ["France", "Russie", "USA"]
// In my var body: some View...
// Trying to change the value of country of the userStore object
Picker(selection: $userStore.userProfile.country, label: Text("Make a choice")) {
ForEach(0 ..< country.count) { index in
Text(self.country[index]).tag(index)
}
Thank you all for your help.
you could try something like this:
let country = ["France", "Russie", "USA"]
#State var countrySelection = 0
Picker(selection: Binding(get: {
self.countrySelection
}, set: { newVal in
self.countrySelection = newVal
self.userStore.userProfile.country = self.country[self.countrySelection]
}), label: Text("Make a choice")) {
ForEach(0 ..< country.count) { index in
Text(self.country[index]).tag(index)
}
}