Swiftui Section from Attribute of a Struct

SwiftUI Section from attribute of a struct

try this:

struct Person: Identifiable {
var id = UUID()
var name: String
var company: String
}

class PeopleList: ObservableObject {

@Published var people = [
Person(name: "Bob", company: "Apple"),
Person(name: "Bill", company: "Microsoft"),
Person(name: "Brenda", company: "Apple"),
Person(name: "Lucas", company: "Microsoft"),
]

func getGroups() -> [String] {

var groups : [String] = []

for person in people {
if !groups.contains(person.company) {
groups.append(person.company)
}
}
return groups
}
}

struct ContentView: View {
@ObservedObject var peopleList = PeopleList()

var body: some View {
NavigationView {
List () {
ForEach (peopleList.getGroups(), id: \.self) { group in
Section(header: Text(group)) {
ForEach(self.peopleList.people.filter { $0.company == group }) { person in

Text(person.name)
}
}
}
}.listStyle(GroupedListStyle())
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Sample Image

SwiftUI struct intialised using specified variable from another struct

Right now your RankingRow(s) depend(s) on buildingData. If you add the rank (and other
data the graph might need) as a parameter you can simplify the view. Calculation of the
rank is preferably done in (an extension of) the model (or ViewModel).

To iterate over the properties of a building you can implement subscript on Building.
Using an enum prevents the need to check for invalid keys. You can add an extension to
the key to conveniently provide your dataDescription. Use ForEach to iterate over the
building properties in SwiftUI.

import SwiftUI

struct Building {
let overallSqm: String
let publicSqm : String
}

// identifying buildings is necessary for calculating the rank, one of the options
// is to make Buidling conform to equatable
extension Building: Equatable {
static func ==(lhs: Building, rhs: Building) -> Bool {
return lhs.overallSqm == rhs.overallSqm && lhs.publicSqm == rhs.publicSqm
}
}

extension Building {

enum PropertyKey {
case overallSqm
case publicSqm
}

subscript(key: PropertyKey) -> String {
switch key {
case .overallSqm: return self.overallSqm
case .publicSqm: return self.publicSqm
}
}
}

extension Building {
func rank(in buildings: [Building], key: Building.PropertyKey) -> Int {
(buildings.sorted { (Double($0[key]) ?? 0) < (Double($1[key]) ?? 0) }.firstIndex { self == $0 } ?? 0) + 1
}
}

extension Building.PropertyKey {
var dataDescription: String {
switch self {
case .overallSqm: return "Total Space (m2):"
case .publicSqm: return "Public Space (m2):"
}
}
}

struct rankingRow: View {

let building : Building
let dataDescription: String
let dataValue : String
let rank : Int

var body: some View {
HStack {
Text(dataDescription)
.fontWeight(.bold)
Text(dataValue)
Spacer()
RankingNumber(rank: rank, size: 28)
}
}
}

struct RankingNumber: View {
let rank: Int
let size: CGFloat

var body: some View {
Text("Rank: \(rank)")
}
}

struct ContentView: View {

let buildings: [Building] = [
Building(overallSqm: "1000.0", publicSqm: "700.0" ),
Building(overallSqm: "1500.0", publicSqm: "600.0" ),
Building(overallSqm: "2000.0", publicSqm: "1200.0")
]

@State var building: Building = Building(overallSqm: "1000.0", publicSqm: "700.0")

let keys: [Building.PropertyKey] = [.overallSqm, .publicSqm]

func rank(for building: Building, key: Building.PropertyKey) -> Int {
(buildings.sorted { (Double($0[key]) ?? 0) < (Double($1[key]) ?? 0) }.firstIndex { building == $0 } ?? 0) + 1
}

var body: some View {
List {
ForEach(keys, id: \.self) { key in
rankingRow(
building : self.building,
dataDescription: key.dataDescription,
dataValue : self.building[key],
rank : self.rank(for: self.building, key: key))
}
}
}
}

How to update attributes of a struct with TextFields made in ForEach

You just have to focus your View.

import SwiftUI

struct ExpandingMenuView: View {
@State var menu: [ItemList] = [
ItemList(name: "Milk Tea", picture: "", list: [ItemModel(name: "Classic Milk Tea"), ItemModel(name: "Taro milk tea")]),
ItemList(name: "Tea", picture: "", list: [ItemModel(name: "Black Tea"), ItemModel(name: "Green tea")]),
ItemList(name: "Coffee", picture: "", list: [])

]
var body: some View {
List{
//This particular setup is for iOS15+
ForEach($menu) { $itemList in
ItemListView(itemList: $itemList)
}
}
}
}

struct ItemListView: View {
@Binding var itemList: ItemList
@State var newItemName: String = ""
var body: some View {
Section(header: Text(itemList.name)) {
ForEach(itemList.list) { item in
Text(item.name)
}
TextField("New Type:", text: $newItemName, onCommit: {
//When the user commits add to array and clear the new item variable
itemList.list.append(ItemModel(name: newItemName))
newItemName = ""
})
}
}
}
struct ItemList: Identifiable, Codable {
var id: UUID = UUID()
var name: String
var picture: String
var list: [ItemModel]
//@State is ONLY for SwiftUI Views
//@State var newItemName: String
}
struct ItemModel: Identifiable, Codable {
var id: UUID = UUID()
var name: String

}
struct ExpandingMenuView_Previews: PreviewProvider {
static var previews: some View {
ExpandingMenuView()
}
}

If you aren't using Xcode 13 and iOS 15+ there are many solutions in SO for Binding with array elements. Below is just one of them

ForEach(menu) { itemList in
let proxy = Binding(get: {itemList}, set: { new in
let idx = menu.firstIndex(where: {
$0.id == itemList.id
})!
menu[idx] = new
})
ItemListView(itemList: proxy)
}

Also note that using indices is considered unsafe. You can watch Demystifying SwiftUI from WWDC2021 for more details.

SwiftUI Can't Put Sections Inside Other Containers

According to the SwiftUI doc, you're advised to use Sections inside supported parent views such as List, Picker, and Form.
(Use Section instances in views like List, Picker, and Form to organize content into separate sections.)

The issue is that you're trying to divide unsupported views into sections. Native containers such as Sections sometimes have strict constraints to allow a more consistent design system.

Try putting sections inside a List, and it should work as you've desired, although in this case I would build a custom section view.

    struct Group2: View {
var thing: Thing

var body: some View {
HStack {
// Assuming you want to put two Sections horizontally (which is not the best usage of SwiftUI native sections)
// You can put two separate Lists inside HStack, and a section in each List.
List {
Section {
Text(thing.name)
} header: {
Text("Thing Name:")
}//section
}
List {
Section {
Text(thing.name)
} header: {
Text("Thing Name:")
}//section

}
}
}
}//group 2

iPhone
iPad

ForEach Identifiable Struct Object Properties Into List Ordered By categoryName

You have flat array and just iterate though it several times, so result is also flat multiplied several times.

Ok, for model you selected, ie items, the result you tried to accomplish can be reached with the following...

List {
ForEach(Array(Set(items.compactMap{ $0[keyPath: \.categoryName] })), id: \.self) { category in
Section(header: Text(category)) {
ForEach(items.filter { $0.categoryName == category }) { currentItem in
NavigationLink(destination: Text("ItemDetail(itemData: currentItem)")){ Text("\(currentItem.name)") }
}
}
}
}
.listStyle(GroupedListStyle())

However I would select different model for items, ie. dictionary as [categoryName: Item]

SwiftUI: Binding on property of struct derived from environment object

No, you're not necessarily doing something against best practices. I think in SwiftUI, the concepts of data model storage and manipulation quickly become more complex than, for example, what Apple tends to show in its demo code. With a real app, with a single source of truth, like you seem to be using, you're going to have to come up with some ways to bind the data to your views.

One solution is to write Bindings with your own get and set properties that interact with your ObservableObject. That might look like this, for example:

struct TaskView : View {
var taskIdentifier: String // Passed from parent view

@EnvironmentObject private var taskStore: TaskStore

private var taskBinding : Binding<Task> {
Binding {
taskStore.tasks[taskIdentifier] ?? .init(identifier: "", title: "", tags: [])
} set: {
taskStore.tasks[taskIdentifier] = $0
}
}

var body: some View {
TextField("Task title", text: taskBinding.title)
}
}

If you're averse to this sort of thing, one way to avoid it is to use CoreData. Because the models are made into ObservableObjects by the system, you can generally avoid this sort of thing and directly pass around and manipulate your models. However, that doesn't necessarily mean that it is the right (or better) choice either.

You may also want to explore TCA which is an increasingly popular state management and view binding library that provides quite a few built-in solutions for the type of thing you're looking at doing.

How to update the values of a struct in a View

For the use-case as provided the most appropriate is to use view model as in example below

class RecipeViewModel: ObservableObject {
@Published var recipe: Recipe
init(_ recipe: Recipe) {
self.recipe = recipe
}
}

so in the view

struct ContentView: View {
@ObservedObject var recipeVM = RecipeViewModel(Recipe(user: 1231, recipeName: "Recipe Name", createdDate: "SomeDate", ingredients: [Ingredient(ingredient: 1, size: 2)], equipment: [[1],[4],[5]]))

var body: some View {
VStack {
Text(self.recipeVM.recipe.recipeName)
Button(action: {
self.recipeVM.recipe.recipeName = "New Name"
}) {
Text("Change Name")
}
}
}
}

How to pass one SwiftUI View as a variable to another View struct

To sum up everything I read here and the solution which worked for me:

struct ContainerView<Content: View>: View {
@ViewBuilder var content: Content

var body: some View {
content
}
}

This not only allows you to put simple Views inside, but also, thanks to @ViewBuilder, use if-else and switch-case blocks:

struct SimpleView: View {
var body: some View {
ContainerView {
Text("SimpleView Text")
}
}
}

struct IfElseView: View {
var flag = true

var body: some View {
ContainerView {
if flag {
Text("True text")
} else {
Text("False text")
}
}
}
}

struct SwitchCaseView: View {
var condition = 1

var body: some View {
ContainerView {
switch condition {
case 1:
Text("One")
case 2:
Text("Two")
default:
Text("Default")
}
}
}
}

Bonus:
If you want a greedy container, which will claim all the possible space (in contrary to the container above which claims only the space needed for its subviews) here it is:

struct GreedyContainerView<Content: View>: View {
@ViewBuilder let content: Content

var body: some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

If you need an initializer in your view then you can use @ViewBuilder for the parameter too. Even for multiple parameters if you will:

init(@ViewBuilder content: () -> Content) {…}

Get specific attribute of object from a variable in Swiftui

In Swift, you can't access properties by Strings.

Here is one possible solution:

List {
ForEach(0 ..< listRank.count, id: \.self) { index in
HStack {
Text("\(listRank[index].playerName)")
Spacer()
if selectedIndex == 0 {
Text(String(listRank[index].highScore))
} else {
Text(String(listRank[index].attempt))
}
}
}

Use KeyPaths

If all of your properties that you want to display have the same type (for example, Int), then KeyPaths are a good solution.

let rankChoice = [\Player.highscore, \Player.attempt]

and then:

Text(String(listRank[index][keyPath: rankChoice[selectedIndex]]))


Related Topics



Leave a reply



Submit