I have this specific data that I want to use for my app.
struct AssetData : Identifiable {
var id: Int
var title: String
var image: Image
var price: Int
var pricePerSec: Int
var own: Bool
}
class AssetDatas : ObservableObject {
#Published var assetData: [AssetData] = []
init() {
getAssetData()
}
func getAssetData() {
let asset1 = AssetData(id: 0, title: "株",image: Image("asset1"), price: 1000, pricePerSec: 10000, own: false)
let asset2 = AssetData(id: 1, title: "時計",image: Image("asset2"), price: 2000, pricePerSec: 20000, own: false)
let asset3 = AssetData(id: 2, title: "車",image: Image("asset3"), price: 3000 , pricePerSec: 50000, own: false)
let asset4 = AssetData(id: 3, title: "家",image: Image("asset4"), price: 4000, pricePerSec: 50000, own: false)
let asset5 = AssetData(id: 4, title: "ダイヤモンド",image: Image("asset5"), price: 5000, pricePerSec: 50000, own: false)
let asset6 = AssetData(id: 5, title: "船",image: Image("asset6"), price: 6000, pricePerSec: 50000, own: false)
let asset7 = AssetData(id: 6, title: "飛行機",image: Image("asset7"), price: 7000, pricePerSec: 50000, own: false)
let asset8 = AssetData(id: 7, title: "人工衛星",image: Image("asset8"), price: 8000, pricePerSec: 50000, own: false)
let asset9 = AssetData(id: 8, title: "月",image: Image("asset9"), price: 9000, pricePerSec: 50000, own: false)
let asset10 = AssetData(id: 9, title: "宇宙人",image: Image("asset10"), price: 10000, pricePerSec: 50000, own: false)
self.assetData.append(contentsOf: [
asset1,
asset2,
asset3,
asset4,
asset5,
asset6,
asset7,
asset8,
asset9,
asset10
])
}
}
I am looking for a way to change a value of an item in the array such as "price" or "own" when the user clicks a button in a view. Is this possible?
my view is something like this
struct AssetListView : View {
var level = 1
#EnvironmentObject var user : UserData
#StateObject var asset = AssetDatas()
#State private var buttonBackColor:Color = .yellow
#State private var buttonText: String = "買う"
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
var body: some View {
ScrollView{
VStack(alignment: .leading){
ForEach(asset.assetData) { assets in
HStack{
assets.image
.resizable()
.frame(width:50, height: 50)
Text(assets.title)
.font(.headline)
Spacer()
Text("金額: \(assets.price)円")
Button(action: {
if user.pocketMoney >= assets.price && buttonText == "買う"{
user.pocketMoney -= assets.price
self.buttonText += "売る"
self.buttonBackColor = .blue
};if buttonText == "売る"{
buttonText = "買う"
self.buttonBackColor = .blue
user.pocketMoney += assets.price
} else {
}
}) {
Text(buttonText)
.padding(10)
.background(buttonBackColor)
.cornerRadius(15)
}
}
}
.foregroundColor(.white)
.padding()
.background(Color("Color3").cornerRadius(10).opacity(0.8))
.padding(.horizontal)
}
}
}
}
In the view, I have a button, and when I click the button, I want the price of the button to change, and I also want to change the bool of "own". How can I do that?
In iOS 15+, you can use a simple syntax to get a binding to the element in your ForEach that you wish to modify:
struct ContentView : View {
#StateObject var assets = AssetDatas()
var body: some View {
ForEach($assets.assetData) { $asset in
HStack {
Text("\(asset.price)")
Button("Change price") {
asset.price = 0
}
}
}
}
}
Prior to the introduction of that feature, you could provide a custom binding:
//Inside AssetDatas
func bindingForAsset(id: Int) -> Binding<AssetData> {
.init {
self.assetData.first(where: { $0.id == id })!
} set: { newValue in
self.assetData = self.assetData.map { $0.id == id ? newValue : $0 }
}
}
struct ContentView : View {
#StateObject var assets = AssetDatas()
var body: some View {
ForEach(assets.assetData) { asset in
HStack {
Text("\(asset.price)")
Button("Change price") {
assets.bindingForAsset(id: asset.id).wrappedValue.price = 0
}
}
}
}
}
You can use a ForEach element binding. See here for more information on what that is. In simple terms, it allows you to pass in a Binding into your ForEach, and then you can get a Binding for each element.
In this example, you give $asset.assetData (Binding<[AssetData]>) and get $asset (Binding<AssetData>), asset (AssetData), and _asset (not needed).
The three changes are marked with <- HERE. Code:
var body: some View {
ScrollView{
VStack(alignment: .leading){
ForEach($asset.assetData) { $assets in // <- HERE
HStack{
assets.image
.resizable()
.frame(width:50, height: 50)
Text(assets.title)
.font(.headline)
Spacer()
Text("金額: \(assets.price)円")
Button(action: {
assets.price = 10 // <- HERE
assets.own = true // <- HERE
if user.pocketMoney >= assets.price && buttonText == "買う"{
user.pocketMoney -= assets.price
self.buttonText += "売る"
self.buttonBackColor = .blue
};if buttonText == "売る"{
buttonText = "買う"
self.buttonBackColor = .blue
user.pocketMoney += assets.price
} else {
}
}) {
Text(buttonText)
.padding(10)
.background(buttonBackColor)
.cornerRadius(15)
}
}
}
.foregroundColor(.white)
.padding()
.background(Color("Color3").cornerRadius(10).opacity(0.8))
.padding(.horizontal)
}
}
}
Related
Here is the desired result (a List from a viewModel derived using functions to generate yearly pay raises and show user's age from year to year). When the user alters their birthdate and hiredate in the ProfileFormView, this List should update the Age and Year group accordingly:
Unfortunately I have not been able to successfully create this view due to errors in attempts to incorporated functions within the viewModel's array.
Here is my viewModel:
class WorkerViewModel:ObservableObject {
#Published var userData: Worker =
Worker(name: "Robert", birthdate: Date(timeIntervalSince1970: 10000), hiredate: Date(timeIntervalSince1970: 30000), department: "HR")
func calcAge(birthdate: String) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
let birthdayDate = dateFormater.date(from: birthdate)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthdayDate!, to: now, options: [])
let age = calcAge.year
return age!
}
func calcYearGroup(hiredate: String) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
let newHireDateFormatted = dateFormater.date(from: hiredate)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcYearGroup = calendar.components(.year, from: newHireDateFormatted!, to: now, options: [])
let yearGroup = calcYearGroup.year! + 1
return yearGroup
}
func calcAnnualEarnings(hourlyRate: Double, monthlyHours: Double) -> Double {
return (hourlyRate * monthlyHours) * 12
}
#Published var userDataList: [WorkerInfo] = [
WorkerInfo(age: calcAge(birthdate: $WorkerInfo.birthdate), yearGroup: calcYearGroup(hiredate: $WorkerInfo.hiredate), salary: calcAnnualEarnings(hourlyRate: PayRates.hR[1], monthlyHours: 72), dept: "\($WorkerInfo.department)"),
WorkerInfo(age: calcAge(birthdate: $WorkerInfo.birthdate) + 1, yearGroup: calcYearGroup(hiredate: $WorkerInfo.hiredate) + 1, salary: calcAnnualEarnings(hourlyRate: PayRates.hR[2], monthlyHours: 72), dept: "\($WorkerInfo.department)"),
WorkerInfo(age: calcAge(birthdate: $WorkerInfo.birthdate) + 2, yearGroup: calcYearGroup(hiredate: $WorkerInfo.hiredate) + 2, salary: calcAnnualEarnings(hourlyRate: PayRates.hR[3], monthlyHours: 72), dept: "\($WorkerInfo.department)"),
WorkerInfo(age: calcAge(birthdate: $WorkerInfo.birthdate) + 3, yearGroup: calcYearGroup(hiredate: $WorkerInfo.hiredate) + 3, salary: calcAnnualEarnings(hourlyRate: PayRates.hR[4], monthlyHours: 72), dept: "\($WorkerInfo.department)"),
WorkerInfo(age: calcAge(birthdate: $WorkerInfo.birthdate) + 4, yearGroup: calcYearGroup(hiredate: $WorkerInfo.hiredate) + 4, salary: calcAnnualEarnings(hourlyRate: PayRates.hR[5], monthlyHours: 72), dept: "\($WorkerInfo.department)"),
]
}
Here is my model data:
struct Worker: Codable, Identifiable {
var id = UUID()
var name: String
var birthdate: Date
var hiredate: Date
var department: String
}
struct WorkerInfo: Codable, Identifiable {
var id = UUID()
var age: Int
var yearGroup: Int
var salary: Double
var dept: String
}
struct MockData {
static let sampleUser = WorkerInfo(age: 41, yearGroup: 1, salary: 80000, dept: "HR")
}
struct PayRates {
let hR: [Double] = [0,92,128,150,154,158,162,166,170,172,174,177,180]
let management: [Double] = [0,235,236,238,240,242,244,246,248,250,252,254,256]
}
Here is my Content View:
struct ContentView: View {
#StateObject var vm = WorkerViewModel()
var body: some View {
TabView {
ProfileFormView()
.tabItem {
Image(systemName: "square.and.pencil")
Text("Profile")
}
WorkerView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
.padding()
}.environmentObject(vm)
}
}
Here is my Worker View:
struct WorkerView: View {
#EnvironmentObject var vm: WorkerViewModel
var body: some View {
ZStack {
List($vm.userDataList) { $worker in
WorkerCardView(workerInfo: $worker)
}
}
}
}
Here is my ProfileForm View:
struct ProfileFormView: View {
#EnvironmentObject var vm: WorkerViewModel
#State var depts = ["HR","Management","Marketing","Development"]
var body: some View {
Form {
Section(header: Text("Personal Information")) {
TextField("Name", text: $vm.userData.name)
DatePicker("Birthdate", selection: $vm.userData.birthdate, displayedComponents: .date)
DatePicker("New Hire Date", selection: $vm.userData.hiredate, displayedComponents: .date)
Picker("Department", selection: $vm.userData.department) {
ForEach(depts, id: \.self) {
Text ($0)
}
}
}
}
}
}
And lastly, here is my WorkerCard View:
struct WorkerCardView: View {
#Binding var workerInfo: WorkerInfo
var body: some View {
HStack{
VStack(alignment: .leading) {
Text("Year \(workerInfo.yearGroup)")
.font(.headline)
Label("Age \(workerInfo.age)", systemImage: "person")
.foregroundStyle(.blue)
.font(.subheadline)
}
Spacer()
VStack(alignment: .leading) {
Text("$ \(workerInfo.salary, specifier: "%.0f") salary")
.font(.headline)
Label("\(workerInfo.dept) Dept", systemImage: "building")
.foregroundStyle(.blue)
.font(.subheadline)
}
}
}
}
Thank you in advance for your help!!
//UPDATE 1//
I have converted my functions to computed properties seemingly error free. This also has an added benefit of making the code cleaner which is nice.
I have boiled the problem down now to an initialization issue. Each of my three computed properties trigger compiler errors when placed in the userDataList array:
"Cannot use instance member 'computedProperty' within property initializer; property initializers run before 'self' is available"
//UPDATE 2//
I converted the userDataArray into a computed property, but the resulting compiler error is related to the #Published property wrapper. Naturally, computed properties cannot receive property wrappers, but I need the userDataArray to be visible to the WorkerView in order for the List to iterate over the userDataArray.
Removing the #Published causes this complier error in the WorkerView:
"Cannot assign to property: 'userDataList' is a get-only property"
I have a project I'm working on that I started back in early 2020. It was working fine when I stopped on it. I recently started it back up and now three views no longer work. The only common ground I can find is that they are the only swift files that have ForEach in them. The code for the three vies is attached below.
I have tried commenting out blocks of code and the issue seems to revolve around the filteredDeals and the ForEach loop using that same variable. It comes from a view model, which that code is also attached below.
I'm new to the stack overflow forum so if there is other relevant information I can provide please let me know. Thank you in advance!
First View
import SwiftUI
import UIKit
import Firebase
import Foundation
struct DealList: View {
//Set the background color to clear
init(){
UITableView.appearance().backgroundColor = .clear
UINavigationBar.appearance().backgroundColor = .clear
}
//Activate the viewmodel
#ObservedObject private var viewModel = DealsViewModel()
//Activate User settings
#EnvironmentObject var settings: GlobalSettings
//View design
var body: some View {
NavigationView {
ZStack {
Color(#colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1))
.edgesIgnoringSafeArea(.all)
VStack {
ScrollView(.vertical, showsIndicators: false) {
VStack {
HStack {
Text("Food Deals")
.bold()
.font(.title)
.foregroundColor(Color.white)
.padding()
.padding(.leading,10)
Spacer()
Text("Selected day is " + "\(settings.SelectedDay)")
.foregroundColor(.white)
.padding(.horizontal,8)
//Navigation links that allows the user to filter by day of the week
NavigationLink(destination: FoodFilterForm(),
label: {
Text("Filter")
.bold()
.padding(.all, 10)
.background(Color.gray)
.cornerRadius(5)
.shadow(radius:5)
.foregroundColor(.white)
.padding(.trailing, 10)
})
}
let filterDeals: [Array] = "\(settings.SelectedDay)" == "Everyday" ? viewModel.deals : viewModel.deals.filter { $0.dayofweek == "\(settings.SelectedDay)" }
// Loop through each deal and display them in a list
ForEach(filterDeals) { deals in
NavigationLink (
destination: DealDetails (deals: deals),
label: {
HStack(spacing: 6.0) {
//Image(systemName: "circle")
// .resizable()
//.frame(width:25, height:25)
Text("\(deals.dealInfo)" + " # " + "\(deals.restaurant)" + " on " + "\(deals.dayofweek)")
.fontWeight(.light)
.frame(minWidth: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, idealWidth: 400, maxWidth: 400, minHeight: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, idealHeight: 50, maxHeight: 150, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.multilineTextAlignment(.center)
.padding(.vertical, 7)
.padding(.horizontal,10)
.foregroundColor(Color.white)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 2)
.padding(.horizontal, 5)
.background(Color(#colorLiteral(red: 0.1019607857, green: 0.2784313858, blue: 0.400000006, alpha: 1)))
.cornerRadius(10)
.shadow(radius: 10)
.padding(.horizontal,8)
})
}
}
}
}
// Hide the navigation bar in the navigation view
}.navigationBarHidden(true)
.onAppear() {
self.viewModel.fetchData()
}
}
}
}
Second View
import SwiftUI
import UIKit
import Firebase
import Foundation
struct DrinkList: View {
//Set the background color to clear
init(){
UITableView.appearance().backgroundColor = .clear
}
//Assign a value to deals to be called later
#ObservedObject private var viewModel = DrinksViewModel()
//Activate User settings
#EnvironmentObject var settings: GlobalSettings
//View design
var body: some View {
NavigationView {
ZStack {
Color(#colorLiteral(red: 0.05882352963, green: 0.180392161, blue: 0.2470588237, alpha: 1))
.edgesIgnoringSafeArea(.all)
VStack {
ScrollView(.vertical, showsIndicators: false) {
VStack {
HStack {
Text("Drink Deals")
.bold()
.font(.title)
.foregroundColor(Color.white)
.padding()
.padding(.leading,10)
Spacer()
Text("Selected day is " + "\(settings.drinkSelectedDay)")
.foregroundColor(.white)
//Navigation links that allows the user to filter by day of the week
NavigationLink(destination: DrinkFilterForm(),
label: {
Text("Filter")
//.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
.padding(.all, 10)
.background(Color.gray)
.cornerRadius(5)
.shadow(radius:5)
.foregroundColor(.white)
.padding(.trailing, 10)
})
}
/* // Loop through each deal and display them in a list
ForEach(viewModel.drinks) { drinks in
NavigationLink (
destination: DrinkDetails (drinks: drinks),
label: {
HStack(spacing: 8.0) {
//Image(systemName: "circle")
// .resizable()
//.frame(width:25, height:25)
Text("\(drinks.dealInfo)" + " # " + "\(drinks.restaurant)" + " on " + "\(drinks.dayofweek)")
.fontWeight(.light)
.frame(minWidth: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, idealWidth: 400, maxWidth: 400, minHeight: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, idealHeight: 50, maxHeight: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.multilineTextAlignment(.center)
.padding(.vertical, 7)
.padding(.horizontal,10)
.foregroundColor(Color.white)
}
.padding(.vertical, 2)
.padding(.horizontal, 5)
.background(Color(#colorLiteral(red: 0.1019607857, green: 0.2784313858, blue: 0.400000006, alpha: 1)))
.cornerRadius(10)
.shadow(radius: 10)
.padding(.horizontal,8)
})
} */
}
}
}
// Hide the navigation bar in the navigation view
}.navigationBarHidden(true)
.onAppear() {
self.viewModel.fetchData()
}
}
}
}
Data Model
import Foundation
import Firebase
import FirebaseFirestore
class DealsViewModel: ObservableObject {
#Published var deals = [Deal]()
var db = Firestore.firestore()
func fetchData() {
db.collection("storeDeals").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.deals = documents.map { queryDocumentSnapshot -> Deal in
let data = queryDocumentSnapshot.data()
let dealInfo = data["dealInfo"] as? String ?? ""
let restaurant = data["restaurant"] as? String ?? ""
let dayofweek = data["dayofweek"] as? String ?? ""
let imageDestination = data["imageDestination"] as? String ?? ""
let dealImage = data["dealIamge"] as? String ?? ""
let cityAddress = data["cityAddress"] as? String ?? ""
let address = data["address"] as? String ?? ""
let phoneNumber = data["phoneNumber"] as? String ?? ""
let latitude = data["latitude"] as? Double ?? 0
let longitude = data["longitude"] as? Double ?? 0
let isFeatured = data["isFeatured"] as? Bool ?? false
let isManagerAccepted = data["isManagerAccepted"] as? Bool ?? false
let startTime = data["startTime"] as? String ?? ""
let endTime = data["endTime"] as? String ?? ""
return Deal(dealInfo: dealInfo, restaurant: restaurant, dayofweek: dayofweek, imageDestination: imageDestination, dealImage: dealImage, cityAddress: cityAddress, address: address, phoneNumber: phoneNumber, latitude: latitude, longitude: longitude, isFeatured: isFeatured, isManagerAccepted: isManagerAccepted, startTime: startTime, endTime: endTime)
}
}
}
}
I have an array of items and I want to change its foreground color when tapped. Right now with the below code all of them are changing to blue. How should I modify it?
struct HorizontalCardSelector: View {
#State var selected = 0
#State var items: [String] = ["Visa", "MasterCard", "PayPal"]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 17) {
ForEach(items, id: \.self) { item in
Button(action: {
self.selected = items.firstIndex(of: item)!
print("item \(item) tapped")
}, label: {
Text(item)
.foregroundColor(self.selected == selected ? .blue: .black)
.fontWeight(.semibold)
})
}
}
}
.padding(.vertical)
}
}
Change your selected variable to an Optional string. Then, compare whether or not the current item is equal to the selected variable.
struct ContentView: View {
#State var selected : String? // <-- Here
#State var items: [String] = ["Visa", "MasterCard", "PayPal"]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 17) {
ForEach(items, id: \.self) { item in
Button(action: {
self.selected = item // <-- Here
print("item \(item) tapped")
}, label: {
Text(item)
.foregroundColor(self.selected == item ? .blue: .black) // <-- Here
.fontWeight(.semibold)
})
}
}
}
.padding(.vertical)
}
}
You can maintain a boolean state array, that hold tapped state for each item in items array. Then you can toggle state between true and false.
import SwiftUI
struct HorizontalCardSelector: View {
#State var selected :[Bool]
#State var items: [String]
init(item:[String]) {
_items = State(wrappedValue: item)
_selected = State(wrappedValue: [Bool](repeating: false, count: item.count))
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 17) {
ForEach(0..<items.count, id: \.self) { index in
Button(action: {
if self.selected[index] == true{
self.selected[index] = false
}else{
self.selected[index] = true
}
print("item \(items[index]) tapped")
}, label: {
Text(items[index])
.foregroundColor(selected[index] ? .blue: .black)
.fontWeight(.semibold)
})
}
}
}
.padding(.vertical)
}
}
#main
struct WaveViewApp: App {
var body: some Scene {
WindowGroup {
HorizontalCardSelector(item: ["Visa", "MasterCard", "PayPal"])
}
}
}
Array initialisation depends on your requirement.You can perform from child view as well.
This is my CheckBoxField
How do I display a checkboxfield with a checkmark shows it was pre-checked already?
Here is how I use it. I would like to see the checkboxfield has a checkmark in there when loading like this one.
CheckboxField(
id: "Completed",
label: "Completed",
callback: self.checkboxSelected
)
func checkboxSelected(id: String, isMarked: Bool) {
print("This is done ->>> \(id) is marked: \(isMarked)")
}
import SwiftUI
struct CheckboxField: View {
let id: String
let label: String
let size: CGFloat
let color: Color
let textSize: Int
let callback: (String, Bool)->()
init(
id: String,
label:String,
size: CGFloat = 10,
color: Color = Color.black.opacity(0.68),
textSize: Int = 14,
callback: #escaping (String, Bool)->()
) {
self.id = id
self.label = label
self.size = size
self.color = color
self.textSize = textSize
self.callback = callback
}
#State var isMarked:Bool = false
var body: some View {
Button(action:{
self.isMarked.toggle()
self.callback(self.id, self.isMarked)
}) {
HStack(alignment: .center, spacing: 10) {
Image(systemName: self.isMarked ? "checkmark.square" : "square")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(label)
.font(Font.system(size: size))
.foregroundColor(Color.black.opacity(0.87))
Spacer()
}.foregroundColor(self.color)
}
.foregroundColor(Color.white)
}
}
You can make
#State var isMarked:Bool = false
Into a Binding instead.
#Binding var isMarked: Bool
This way, you can access isMarked from both the superview and the checkmark view itself. You can also avoid using a closure.
Example:
struct ContentView: View {
#State var firstMarked = false
#State var secondMarked = true /// at load, the second one will be checked already
#State var thirdMarked = false
var body: some View {
VStack {
CheckboxField(id: "Completed", label: "Completed", isMarked: $firstMarked)
CheckboxField(id: "Completed", label: "Completed", isMarked: $secondMarked)
CheckboxField(id: "Completed", label: "Completed", isMarked: $thirdMarked)
}
.padding()
}
}
struct CheckboxField: View {
let id: String
let label: String
let size: CGFloat
let color: Color
let textSize: Int
#Binding var isMarked: Bool /// Binding here!
init(
id: String,
label:String,
size: CGFloat = 10,
color: Color = Color.black.opacity(0.68),
textSize: Int = 14,
isMarked: Binding<Bool>
) {
self.id = id
self.label = label
self.size = size
self.color = color
self.textSize = textSize
self._isMarked = isMarked /// to init, you need to add a _
}
var body: some View {
Button(action:{
self.isMarked.toggle() /// just toggle without closure
}) {
HStack(alignment: .center, spacing: 10) {
Image(systemName: self.isMarked ? "checkmark.square" : "square")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(label)
.font(Font.system(size: size))
.foregroundColor(Color.black.opacity(0.87))
Spacer()
}.foregroundColor(self.color)
}
.foregroundColor(Color.white)
}
}
Result:
Change your isMarked State from false to true. Because #State should only be used in the current view, it's a best practice to make the isMarked variable private.
#State private var isMarked: Bool = true
This makes your Button checked when the view is getting loaded.
You can then access your button's state using #Binding in other views. The Binding and State variables have to have the same name otherwise, the Binding doesn`t know what to reference.
This is a very similar problem to one I had before (which no one could answer). I'm trying to create a dynamic list in which I can edit elements. As far as I can gather, the recommended way to do this is to have an EditView, with bindings, that's activated by a NavigationLink in the LIst.
So, I've done that. It appears to work at first, until I realised that each NavigationLink would only work once (is this a bug?). I can't think what I could have done wrong to cause that.
Then I thought perhaps I can switch to in-place editing by having the EditView in the List. I devised a theoretical way to do this, then tried it in my code. And at first it seemed to work great. However, if 'edit in place' is on, deleting the last element causes 'Fatal error: Index out of range'.
I've bundled my whole code into one file so you can just copy and paste into Xcode to try for yourself.
I'm starting to think that maybe XCode 11.3.1 is far from the finished article, yet.
import SwiftUI
struct EditView: View {
#Binding var person:Person
var body: some View {
HStack{
Group{
TextField("name1", text: $person.name1)
TextField("name2", text: $person.name2)
}.frame(width:200)
.font(.headline)
.padding(.all, 3)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
}.navigationBarTitle("Edit entry")
}
}
struct Person:Identifiable, Equatable{
var id:UUID
var name1:String
var name2:String
var isEditable:Bool
}
class PersonList: ObservableObject {
#Published var individuals = [Person]()// Array of Person structs
}
struct ContentView: View {
#ObservedObject var people = PersonList()// people.individuals = [Person] array
#State private var edName1:String = "" //temporary storage for adding new member
#State private var edName2:String = "" //temporary storage for adding new member
#State private var allowEditing:Bool = false
var elementCount:Int{
let c = people.individuals.count
return c
}
// arrays for testing - adds random names from these (if input field '1st name' is empty)...
var firstNames = ["Nick","Hermes","John","Hattie","Nicola","Alan", "Dwight", "Richard","Turanga", "Don","Joey"]
var surnames = ["Farnsworth","Fry","Wong","Zoidberg","Conrad","McDougal","Power","Clampazzo","Brannigan","Kroker","Leela"]
var body: some View {
NavigationView{
VStack{
HStack{
Text("Add person:")
.padding(.all, 5)
.frame(alignment: .leading)
TextField("1st name", text: $edName1)
.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.blue, lineWidth: 2))
TextField("2nd name", text: $edName2)
.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.blue, lineWidth: 2))
// 🆗 Button...
Image(systemName: "plus.circle")
.font(.largeTitle)
.foregroundColor(.orange)
.onTapGesture {
if self.edName1 == ""{
self.edName1 = self.firstNames.randomElement() ?? "⁉️"
self.edName2 = self.surnames.randomElement() ?? "⁉️"
}
self.people.individuals.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2, isEditable: false))
self.edName1 = ""
self.edName2 = ""
print("Element count: \(self.elementCount)")
}
Toggle(isOn: $allowEditing){Text("edit in place")}.padding(.all,5).overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.red, lineWidth: 2))
Spacer()
// 🆗 Button...sort
Image(systemName: "arrow.up.arrow.down.square")
.font(.title)
.padding(.all,4)
.foregroundColor(.blue)
.onTapGesture {
self.people.individuals.sort{ // sort list alphabetically by name2
$0.name2 < $1.name2
}
}
// 🆗 Button...reverse order
Image(systemName: "arrow.uturn.up.square")
.font(.title)
.padding(.all,8)
.foregroundColor(.blue)
.onTapGesture {
self.people.individuals.reverse()
}
}.padding(.all,8)
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange, lineWidth: 2))
List{
ForEach(people.individuals){individual in
HStack{
if self.allowEditing{
//Toggle to edit in place
Toggle(isOn: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!].isEditable){
Text("edit").font(.headline).foregroundColor(.green).opacity(individual.isEditable ? 1.0 : 0.4)
}.frame(width:100)
}
if individual.isEditable{
EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])
}
else{
NavigationLink(destination:EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])){
Text("\(individual.name1) \(individual.name2)")
.frame(width: 200, alignment: .leading)
.padding(.all, 3)
}// link
}
}
}.onDelete(perform: deleteRow)
}
}.navigationBarTitle("People List (\(elementCount))")
}.navigationViewStyle(StackNavigationViewStyle())
}
func deleteRow(at offsets: IndexSet){
self.people.individuals.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.colorScheme, .dark)
}
}
Can anyone shed any light on this? I can't find anything to help me.
UPDATE: Thanks to 'krjw' for pointing out the single use NavLink problem does not happen on a real device.
The 'last element delete' issue seems to be something to do with an active binding being present in the element's view.
Ok despite my comment I tried to get to a solution and I might found an acceptable one:
I had to remodel Person... The whole indices was the issue of course but I couldn't exactly find out when what happens. I even tried with a local #State which updates the view and then updates the array of the #ObservedObject...
here are some links which could help to further investigate though...
Swift UI detail remove
How do I set the toggle state in a foreach loop in SwiftUI
Also this link here shows how to update members of an observed array generically which is pretty cool!:
https://stackoverflow.com/a/57920136/5981293
struct EditView: View {
#ObservedObject var person: Person
var body: some View {
HStack{
Group{
TextField("name1", text: $person.name1)
TextField("name2", text: $person.name2)
}//.frame(width:200)
.font(.headline)
.padding(.all, 3)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
}.navigationBarTitle("Edit entry")
}
}
struct RowView: View {
#Binding var allowEditing: Bool
#ObservedObject var individual: Person
var body: some View {
HStack {
if self.allowEditing {
//Toggle to edit in place
Toggle(isOn: self.$individual.isEditable){
Text("edit").font(.headline).foregroundColor(.green).opacity(self.individual.isEditable ? 1.0 : 0.4)
}//.frame(width:100)
}
if self.individual.isEditable{
EditView(person: self.individual)
}
else{
NavigationLink(destination:EditView(person: self.individual)){
Text("\(self.individual.name1) \(self.individual.name2)")
//.frame(width: 200, alignment: .leading)
.padding(.all, 3)
}// link
}
}
}
}
class Person: ObservableObject, Identifiable {
#Published var id:UUID
#Published var name1:String
#Published var name2:String
#Published var isEditable:Bool
init(id: UUID, name1: String, name2: String, isEditable: Bool){
self.id = id
self.name1 = name1
self.name2 = name2
self.isEditable = isEditable
}
}
struct ContentView: View {
#State var people = [Person]()//try! ObservableArray<Person>(array: []).observeChildrenChanges(Person.self)// people.individuals = [Person] array
#State private var edName1:String = "" //temporary storage for adding new member
#State private var edName2:String = "" //temporary storage for adding new member
#State private var allowEditing:Bool = false
// arrays for testing - adds random names from these (if input field '1st name' is empty)...
var firstNames = ["Nick","Hermes","John","Hattie","Nicola","Alan", "Dwight", "Richard","Turanga", "Don","Joey"]
var surnames = ["Farnsworth","Fry","Wong","Zoidberg","Conrad","McDougal","Power","Clampazzo","Brannigan","Kroker","Leela"]
var body: some View {
NavigationView{
VStack{
HStack{
Text("Add person:")
.padding(.all, 5)
.frame(alignment: .leading)
TextField("1st name", text: $edName1)
//.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.blue, lineWidth: 2))
TextField("2nd name", text: $edName2)
//.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.blue, lineWidth: 2))
// 🆗 Button...
Image(systemName: "plus.circle")
.font(.largeTitle)
.foregroundColor(.orange)
.onTapGesture {
if self.edName1 == ""{
self.edName1 = self.firstNames.randomElement() ?? "⁉️"
self.edName2 = self.surnames.randomElement() ?? "⁉️"
}
self.people.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2, isEditable: false))
self.edName1 = ""
self.edName2 = ""
print("Element count: \(self.people.count)")
}
Toggle(isOn: $allowEditing){Text("edit in place")}.padding(.all,5).overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.red, lineWidth: 2))
Spacer()
// 🆗 Button...sort
Image(systemName: "arrow.up.arrow.down.square")
.font(.title)
.padding(.all,4)
.foregroundColor(.blue)
.onTapGesture {
self.people.sort{ // sort list alphabetically by name2
$0.name2 < $1.name2
}
}
// 🆗 Button...reverse order
Image(systemName: "arrow.uturn.up.square")
.font(.title)
.padding(.all,8)
.foregroundColor(.blue)
.onTapGesture {
self.people.reverse()
}
}.padding(.all,8)
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange, lineWidth: 2))
List {
ForEach(self.people) { person in
RowView(allowEditing: self.$allowEditing, individual: person)
}.onDelete(perform: deleteRow)
}
}.navigationBarTitle("People List (\(self.people.count))")
}.navigationViewStyle(StackNavigationViewStyle())
}
func deleteRow(at offsets: IndexSet){
self.people.remove(atOffsets: offsets)
print(self.people.count)
}
}
I hope this helps!