I'm literally starting programming, so sorry for the rookie question.
I'm working in Xcode/SwiftUI and trying to make a counter where the inial value comes from a JSON file.
I manage to extract the value for strings... but after spending hours trying to find out how to set the initial counter to the value "hull", I'm finally asking help!
My JSON file is formatted as such:
{
"id": 1,
"name": "Chaser",
"type": "Level 1",
"hull": 5,
"shields": 0,
"imageName": "chaser"
},
and my struct is like this:
struct Enemy: Hashable, Codable, Identifiable {
var id: Int
var name: String
var type: String
var hull: Int
var shields: Int
fileprivate var imageName: String
}
In my page, my code is like this:
struct EnemyDetails: View {
#State var count : Int = 0
var enemy : Enemy
var body: some View {
VStack {
EnemyImage(image: Image("EnemyImage"))
.frame(height:300)
VStack {
Spacer()
Text(enemy.name).font(.title)
Text(enemy.type)
Spacer()
HStack {
Button(action: {self.count = self.count - 1}) {
Image("Decrease")
}.padding(20)
Text("\(count)").font(.system(size:100)).padding(20)
Button(action: {self.count = self.count + 1}) {
Image("Increase")
}.padding(20)
}
Spacer()
}
}
}
}
I would like the value "Count" to be the value "Hull" from the JSON file.
Can anyone help?
Many thanks!
Here it is
struct EnemyDetails: View {
var enemy : Enemy
#State private var count : Int // don't initialise here
init(enemy: Enemy) {
self.enemy = enemy
_count = State(initialValue: enemy.hull) // << initial state !!
}
// ... other your code
Related
I have a List, with custom Stepper inside each row. Therefore, when I scroll my stepper is reset. (The increment and decrement works when is visible. When it disappear, it's reset. Don't keep the state. It's alway's reset).
Xcode: v14.2 / Simulator iOS: 16.2
struct Product: Codable, Hashable, Identifiable {
let id: String
let name: String
let step: Int
let quantity: Int
let priceHT: Double
}
class ProductViewModel: ObservableObject {
#Published var products = [Product]()
...
}
struct ProductListView: View {
#EnvironmentObject var productViewModel: ProductViewModel
var body: some View {
List(productViewModel.products) { product in
ProductRowView(product: product)
}
}
}
My List row:
I tried to modify #State with #binding, but without success.
struct ProductRowView: View {
#State var product: Product
var body: some View {
HStack {
VStack {
Text(product.name)
Text(String(format: "%.2f", product.priceHT) + "€ HT")
}
Spacer()
MyStepper(product: $product, value: product.quantity)
.font(.title)
}
}
}
My Custom stepper:
struct MyStepper: View {
#Binding var product: Product
#State var value: Int = 0
var body: some View {
HStack {
VStack() {
HStack {
Button(action: {
value -= product.step
if let row = orderViewModel.productsOrder.firstIndex(where: { $0.name == product.name }) {
let order = Product(id: product.id, name: product.name, step: product.step, quantity: value, priceHT: product.priceHT)
if (value == 0) {
orderViewModel.productsOrder.remove(at: row)
} else {
orderViewModel.productsOrder[row] = order
}
}
}, label: {
Image(systemName: "minus.square.fill")
})
Text(value.formatted())
Button(action: {
value += product.step
let order = Product(id: product.id, name: product.name, step: product.step, quantity: value, priceHT: product.priceHT)
if let row = orderViewModel.productsOrder.firstIndex(where: { $0.name == product.name }) {
orderViewModel.productsOrder[row] = order
} else {
orderViewModel.productsOrder.append(order)
}
}, label: {
Image(systemName: "plus.app.fill")
})
}
Text(product.unit)
}
}
}
}
Thks
EDIT / RESOLVED
Here is the solution for my case :
Change type of quantity. let to var
struct Product: Codable, Hashable, Identifiable {
...
var quantity: Int
...
}
Delete #State in MyStepper and replace value by product.quantity
Use bindings for that, e.g.
struct ProductListView: View {
#EnvironmentObject var model: Model
var body: some View {
List($model.products) { $product in
ProductRowView(product: $product)
}
}
}
struct ProductRowView: View {
#Binding var product: Product // now you have write access to the Product struct
...
However, to make the View reusable and to help with previewing, it's best to pass in only the simple types the View needs, e.g.
struct TitlePriceView: View {
let title: String
#Binding var price: Double
// etc.
TitlePriceView(title: product.title, price: $product.price)
Used this scenario as an example to be a bit more self explanatory. I have a struct which represents a character, and one of the structs attributes is another struct: Stats (where I used id to represent the name in a more simple way).
Besides that I have a view with a ForEach, where I iterate over some Characters and I need to be able to increase a specific stat.
The problem is: I'm trying to increase stat.points using a button, but I keep getting the message "Left side of mutating operator isn't mutable: 'product' is a 'let' constant".
struct Character: Identifiable {
var id: String
var name: String
var stats: [Stats]
}
struct Stats: Identifiable {
var id: String
var points: Int
}
ForEach(characters.stats) { stat in
HStack {
Text("\(stat.id)")
Button {
stat.points += 1
} label: {
Text("Increase")
}
}
}
How could I make this work?
As you have found, structs in your stats: [Stats] does not allow changes
especially in a ForEach where it is a let.
There are many ways to achieve what you want, depending on what you trying to do.
This example code shows the most basic way, using the array of stats: [Stats]
directly:
struct ContentView: View {
#State var characters = Character(id: "1",name: "name-1", stats: [Stats(id: "1", points: 1),Stats(id: "2", points: 1)])
var body: some View {
ForEach(characters.stats.indices, id: \.self) { index in
HStack {
Text("\(characters.stats[index].id)")
Button {
characters.stats[index].points += 1
} label: {
Text("Increase")
}
Text("\(characters.stats[index].points)")
}
}
}
}
Here is another approach, using a function, to increase your stat points value:
struct ContentView: View {
#State var characters = Character(id: "1", name: "name-1", stats: [Stats(id: "1", points: 1), Stats(id: "2", points: 1)])
var body: some View {
ForEach(characters.stats) { stat in
HStack {
Text("\(stat.id)")
Button {
increase(stat: stat)
} label: {
Text("Increase")
}
Text("\(stat.points)")
}
}
}
func increase(stat: Stats) {
if let index = characters.stats.firstIndex(where: {$0.id == stat.id}) {
characters.stats[index].points += 1
}
}
}
In the last few months, many developers have reported NavigationLinks to unexpectedly pop out and some workarounds have been published, including adding another empty link and adding .navigationViewStyle(StackNavigationViewStyle()) to the navigation view.
Here, I would like to demonstrate another situation under which a NavigationLink unexpectedly pops out:
When there are two levels of child views, i.e. parentView > childLevel1 > childLevel2, and childLevel2 modifies childLevel1, then, after going back from level 2 to level 1, level 1 pops out and parentView is shown.
I have filed a bug report but not heard from apple since. None of the known workarounds seem to work. Does someone have an idea what to make of this? Just wait for iOS 15.1?
Below is my code (iPhone app). In the parent view, there is a list of persons from which orders are taken. In childLevel1, all orders from a particular person are shown. Each order can be modified by clicking on it, which leads to childLevel2. In childLevel2, several options are available (here only one is shown for the sake of brevity), which is the reason why the user is supposed to leave childLevel2 via "< Back".
import SwiftUI
struct Person: Identifiable, Hashable {
let id: Int
let name: String
var orders: [Order]
}
struct Pastry: Identifiable, Hashable {
let id: Int
let name: String
}
struct Order: Hashable {
var paId: Int
var n: Int // used only in the real code
}
class Data : ObservableObject {
init() {
pastries = [
Pastry(id: 0, name: "Prezel"),
Pastry(id: 1, name: "Donut"),
Pastry(id: 2, name: "bagel"),
Pastry(id: 3, name: "cheese cake"),
]
persons = [
Person(id: 0, name: "Alice", orders: [Order(paId: 1, n: 1)]),
Person(id: 1, name: "Bob", orders: [Order(paId: 2, n: 1), Order(paId: 3, n: 1)])
]
activePersonsIds = [0, 1]
}
#Published var activePersonsIds: [Int] = []
#Published var persons: [Person] = []
#Published var pastries: [Pastry]
#Published var latestOrder = Order(paId: 0, n: 1)
lazy var pastryName: (Int) -> String = { (paId: Int) -> String in
if self.pastries.first(where: { $0.id == paId }) == nil {
return "undefined pastryId " + String(paId)
}
return self.pastries.first(where: { $0.id == paId })!.name
}
var activePersons : [Person] {
return activePersonsIds.compactMap {id in persons.first(where: {$0.id == id})}
}
}
#main
struct Bretzel_ProApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
#StateObject var data = Data()
var body: some View {
TabView1(data: data)
// in the real code, there are more tabs
}
}
struct TabView1: View {
#StateObject var data: Data
var body: some View {
NavigationView {
List {
ForEach(data.activePersons, id: \.self) { person in
NavigationLink(
destination: EditPerson(data: data, psId: person.id),
label: {
VStack (alignment: .leading) {
Text(person.name)
}
}
)
}
}
.listStyle(PlainListStyle())
.navigationTitle("Orders")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct EditPerson: View {
#ObservedObject var data: Data
var psId: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
List() {
ForEach (0...p.orders.count-1, id: \.self) { loop in
Section(header:
HStack() {
Text("BESTELLUNG " + String(loop+1))
}
) {
EPSubview1(data: data, psId: psId, loop: loop)
}
}
}.navigationTitle(p.name)
.listStyle(InsetGroupedListStyle())
}
}
struct EPSubview1: View {
#ObservedObject var data: Data
var psId: Int
var loop: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
let o1: Order = p.orders[loop]
NavigationLink(
destination: SelectPastry(data: data)
.onAppear() {
data.latestOrder.paId = o1.paId
}
.onDisappear() {
data.persons[pindex].orders[loop].paId = data.latestOrder.paId
},
label: {
VStack(alignment: .leading) {
Text(String(o1.n) + " x " + data.pastryName(o1.paId))
}
}
)
}
}
struct SelectPastry: View {
#ObservedObject var data : Data
var body: some View {
VStack {
List {
ForEach(data.pastries, id: \.self) {pastry in
Button(action: {
data.latestOrder.paId = pastry.id
}) {
Text(pastry.name)
.foregroundColor(data.latestOrder.paId == pastry.id ? .primary : .secondary)
}
}
}.listStyle(PlainListStyle())
}
}
}
The problem is your ForEach. Despite that fact that Person conforms to Identifiable, you're using \.self to identify the data. Because of that, every time an aspect of the Person changes, so does the value of self.
Instead, just use this form, which uses the id vended by Identifiable:
ForEach(data.activePersons) { person in
Which is equivalent to:
ForEach(data.activePersons, id: \.id) { person in
Currently I am trying to update the count in a SingleDay struct inside a Days class from from the TestScreen view.The SingleDay struct is also in an array in Days. The change in count should be reflected in the UpdatingArrayElements view. So far I am running into this error:
Left side of mutating operator isn't mutable: 'day' is a 'let' constant"
and I have absolutely no idea on how to resolve this issue. I would appreciate any help given that I am still a beginner in iOS development and still trying to get the hang of building more complex iOS apps, thanks!
import SwiftUI
struct SingleDay: Identifiable {
let id: String = UUID().uuidString
let day: Int
var count: Int
}
class Days: ObservableObject {
#Published var daysArray: [SingleDay] = []
init() {
daysArray.append(SingleDay(day: 1, count: 0))
}
}
struct UpdatingArrayElements: View {
#StateObject var days: Days = Days()
var body: some View {
NavigationView {
List {
ForEach(days.daysArray) { day in
HStack{
Text("Day: \(day.day)")
Text("Count: \(day.count)")
}
}
}
.navigationBarItems(trailing:
NavigationLink(destination: TestScreen(dayViewModel: days), label: {
Image(systemName: "arrow.right")
.font(.title)
})
)
}
}
}
struct TestScreen: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var dayViewModel: Days
var body: some View {
ZStack {
Color.green.ignoresSafeArea()
VStack {
ForEach(dayViewModel.daysArray) { day in
Text(String(day.day))
Button(action: {
day.count += 1
}, label: {
Text("Add count")
})
}
}
}
}
}
struct UpdatingArrayElements_Previews: PreviewProvider {
static var previews: some View {
UpdatingArrayElements()
}
}
here you go. now we are not using the identifiable so if you want you can also remove it.
so the real problem was that the day which is of type SingleDay is a struct not a class. so foreach give us let not vars , so in case of class object we can easily update properties because they are reference type, but in case of struct we can not do that, so it means day is another copy. so even if we can update the day of struct it will still not update the daysArray element.
struct TestScreen: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var dayViewModel: Days
var body: some View {
ZStack {
Color.green.ignoresSafeArea()
VStack {
ForEach(dayViewModel.daysArray.indices ,id : \.self) { index in
let day = dayViewModel.daysArray[index]
Text("\(day.count)")
Button(action: {
dayViewModel.daysArray[index].count = day.count + 1
print( day.count)
}, label: {
Text("Add count")
})
}
}
}
}
}
in swiftui, I have a state variable count ,which is optional, in the sheet present ,I unwrap the optional and show Detailview, but it seems never hit there.
any idea how why not hit there?
it seems never hit
DetailView(count: num)
import SwiftUI
struct ContentView: View {
#State var showDetailView = false
#State var count : Int?
var testArr = [1,2,3,4,5]
var body: some View {
NavigationView {
List(testArr.indices){ indice in
Text("row num \(indice)")
.onTapGesture{
self.showDetailView = true
self.count = 5
}
}
.sheet(isPresented: self.$showDetailView) {
if let num = self.count{
//never hit here
DetailView(count: num)
}
}
.navigationBarTitle("Your Reading")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailView: View {
var count: Int
var body: some View {
if count == 5 {
Text("5555")
} else {
EmptyView()
}
}
}
The issue is not the optional unwrapping.
The issue is that you are executing update to count while the body is being computed. To overcome this use my solution of:
swiftui state variable count lost what it stored
Let me know if that solution does not work for you.
You could use this workaround (not recommended), if you really have to use #State var count.
DispatchQueue.main.async {
self.count = 5
}
You can use sheet(item:content:) instead of sheet(isPresented:content:):
struct ContentView: View {
#State var count: Int?
var testArr = [1, 2, 3, 4, 5]
var body: some View {
NavigationView {
List(testArr.indices) { index in
Text("row num \(index)")
.onTapGesture {
self.count = 5
}
}
.sheet(item: $count) { count in
DetailView(count: count)
}
.navigationBarTitle("Your Reading")
}
}
}
Note that item must conform to Identifiable, so you need to either use your own struct or create an Int extension:
extension Int: Identifiable {
public var id: Int { self }
}