I have a segmented control that I am using as a tabs in the toolbar of my app linked to the selectedTab variable. when I switch tabs the accounts list changes but it does not reset the lines list. I tried using initialValue on the selected to make sure it reset to 0 but this did not effect it. I tried a print in the init to make sure that the value of selected was 0, after the init. it was every time but it still did not refresh the lines foreach list.
What am I missing?
import SwiftUI
import SQLite3
struct ContentView:View {
#EnvironmentObject var shared:SharedObject
var body: some View {
VStack {
if shared.selectedTab == 0 {
LedgerView(ledger: .Accounts)
} else if shared.selectedTab == 1 {
LedgerView(ledger: .Budgets)
} else if shared.selectedTab == 2 {
ReportsView()
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct LedgerView:View {
#EnvironmentObject var shared:SharedObject
let ledger:LedgerType
#State var selected:Int = 0
init(ledger:LedgerType) {
self.ledger = ledger
self._selected = State(initialValue: 0)
}
var body:some View {
HStack {
VStack(alignment: HorizontalAlignment.leading) {
ForEach(shared.accounts.filter({$0.ledger == ledger})) { account in
Text(account.name)
.background(account.id == self.selected ? Color.accentColor : Color.clear)
.onTapGesture {self.selected = account.id}
}
}
Divider()
VStack(alignment: HorizontalAlignment.leading) {
ForEach(shared.journalLines.filter({$0.accountID == selected})) { line in
Text("Line#\(line.id)")
}
}
}
}
}
struct ReportsView:View {
var body:some View {
Text("Under Construction ...")
}
}
class SharedObject:ObservableObject {
#Published var accounts:[Account] = []
#Published var journalLines:[JournalLine] = []
#Published var selectedTab:Int = 0
init() {
loadData()
}
}
enum LedgerType:Int {
case Accounts=0,Budgets=1
var name:String {
switch(self) {
case .Accounts: return "Accounts"
case .Budgets: return "Budgets"
}
}
}
struct Account:Identifiable {
var id:Int
var name:String
var ledger:LedgerType
}
struct Payee:Identifiable {
var id:Int
var name:String
}
struct Journal:Identifiable {
var id:Int
var date:Date
var payeeID:Int
var memo:String?
}
struct JournalLine:Identifiable {
var id:Int
var journalID:Int
var accountID:Int
var amount:Double
}
edit abridged demo code to try to isolate the problem
import SwiftUI
struct ContentView: View {
#EnvironmentObject var shared:SharedObject
var body: some View {
VStack {
Picker(selection: $shared.selectedTab, label: Text("")) {
Text("Accounts").tag(0)
Text("Budgets").tag(1)
}.pickerStyle(SegmentedPickerStyle())
Divider()
if shared.selectedTab == 0 || shared.selectedTab == 1 {
LedgerView()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct LedgerView:View {
#State var selected:Int = 0
init() {
self._selected = State(initialValue: 0)
print("LedgerView.init()")
}
var body:some View {
VStack(alignment: HorizontalAlignment.leading) {
Text("Selected: \(selected)")
Picker(selection: $selected, label: Text("")) {
Text("Account#1").tag(1)
Text("Account#2").tag(2)
Text("Account#3").tag(3)
}
}
}
}
class SharedObject: ObservableObject {
#Published var selectedTab:Int = 0
}
Based on your recent comment, what you need is #Binding and not #State. To explain: The struct LedgerView is just an extract of the picker in a separate view. When you call LedgerView(), this doesn't instantiate a new object. It simply adds the picker view in that place. Therefore, when you need the picker to reset on switching tabs, you need to use binding to reset the picker. Here's the working code. Hope it helps.
struct ContentView: View {
#EnvironmentObject var shared:SharedObject
#State var selected: Int = 0
var body: some View {
VStack {
Picker(selection: $shared.selectedTab, label: Text("")) {
Text("Accounts").tag(0)
Text("Budgets").tag(1)
}.pickerStyle(SegmentedPickerStyle())
Divider()
if shared.selectedTab == 0 || shared.selectedTab == 1 {
LedgerView(selected: $selected)
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(shared.$selectedTab) { newValue in
self.selected = 0
}
}
}
struct LedgerView:View {
#Binding var selected: Int
var body:some View {
VStack(alignment: HorizontalAlignment.leading) {
Text("Selected: \(selected)")
Picker(selection: $selected, label: Text("")) {
Text("Account#1").tag(1)
Text("Account#2").tag(2)
Text("Account#3").tag(3)
}
}
}
}
[Earlier answer containing alternate solution]
I've modified your code to make the lines work. I've added sample data to get the code to work. I doubt if the problem is with your data. Also I have modified the enum LedgerType to make it iterable. Here's the working code.
I've modified the following in code:
Removed passing ledgerType to LedgerView as the source of truth is
#EnvironmentObject var shared: SharedObject
Added code to select the first account by default when switching tabs. This refreshes the lines when switching between tabs. See the code in .onReceive
Hope this helps. If you need anything, let me know.
struct ContentView:View {
#EnvironmentObject var shared: SharedObject
var body: some View {
VStack {
Picker(selection: $shared.selectedTab, label: Text("")) {
ForEach(0 ..< LedgerType.allCases.count) { index in
Text(LedgerType.allCases[index].rawValue).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
if shared.selectedTab == 0 || shared.selectedTab == 1 {
LedgerView()
} else if shared.selectedTab == 2 {
ReportsView()
}
Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct LedgerView:View {
#EnvironmentObject var shared:SharedObject
#State var selected: Int = 0
var body:some View {
HStack {
VStack(alignment: HorizontalAlignment.leading) {
ForEach(shared.accounts.filter({ $0.ledger == LedgerType.allCases[shared.selectedTab] })) { account in
Text(account.name)
.background(account.id == self.selected ? Color.accentColor : Color.clear)
.onTapGesture {self.selected = account.id}
}
}
Divider()
VStack(alignment: HorizontalAlignment.leading) {
ForEach(shared.journalLines.filter({$0.accountID == selected})) { line in
Text("Line#\(line.id)")
}
}
}
.onReceive(shared.$selectedTab) { newValue in
if let id = self.shared.getInitialAccountId(tabIndex: newValue) {
self.selected = id
}
}
}
}
struct ReportsView:View {
var body:some View {
Text("Under Construction ...")
}
}
class SharedObject:ObservableObject {
#Published var accounts:[Account] = []
#Published var journalLines:[JournalLine] = []
#Published var selectedTab:Int = 0
func getInitialAccountId(tabIndex: Int) -> Int? {
if tabIndex == 0 {
return accounts.filter({
$0.ledger == LedgerType.Accounts
}).first?.id
}
else if tabIndex == 1 {
return accounts.filter({
$0.ledger == LedgerType.Budgets
}).first?.id
}
else {
return accounts.filter({
$0.ledger == LedgerType.Reports
}).first?.id
}
}
init() {
accounts = [
Account(id: 1, name: "Sales", ledger: .Accounts),
Account(id: 2, name: "Purchase", ledger: .Accounts),
Account(id: 3, name: "Forecast", ledger: .Budgets)
]
journalLines = [
// Line for sales
JournalLine(id: 1, journalID: 10, accountID: 1, amount: 200),
JournalLine(id: 2, journalID: 20, accountID: 1, amount: 400),
// Line for purchase
JournalLine(id: 3, journalID: 30, accountID: 2, amount: 600),
JournalLine(id: 4, journalID: 40, accountID: 2, amount: 800)
]
}
}
enum LedgerType: String, CaseIterable {
case Accounts = "Accounts"
case Budgets = "Budgets"
case Reports = "Reports"
}
struct Account:Identifiable {
var id:Int
var name:String
var ledger:LedgerType
}
struct Payee:Identifiable {
var id:Int
var name:String
}
struct Journal:Identifiable {
var id:Int
var date:Date
var payeeID:Int
var memo:String?
}
struct JournalLine:Identifiable {
var id:Int
var journalID:Int
var accountID:Int
var amount:Double
}
shared.accounts doesn't exist. you have never initialized it
[...]
if shared.selectedTab == 0 {
LedgerView(ledger: .Accounts).environmentObject(SharedObject())
} else if shared.selectedTab == 1 {
LedgerView(ledger: .Budgets).environmentObject(SharedObject())
[...]
Edit: Make sure your SceneDelgate also initiates SharedObject otherwise it wont work on physical/simulator device
--
Edit: After running your code and you confirmed you are loading, I ran your code with no change and I don't see any issue, can you look at the gif below and comment what's wrong in what I am seeing?
Related
import SwiftUI
struct ContentView: View {
#State private var set = Set<Int>()
#State private var count = "10"
private let columns:[GridItem] = Array(repeating: .init(.flexible()), count: 3)
#State private var timer:Timer? = nil
#State private var time = 0
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
HStack{
TextField("Create \(count) items", text: $count)
Button {
createSet(count: Int(count)!)
} label: {
Text("Create")
}
}
if let _ = timer {
Text(String(time))
.font(.title2)
.foregroundColor(.green)
}
HStack {
Button {
time = 100
let timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
time -= 10
if time == 0 {
self.timer?.invalidate()
self.timer = nil
}
}
self.timer = timer
} label: {
Text("Start Timer")
}
Button {
self.timer?.invalidate()
self.timer = nil
} label: {
Text("Stop Timer")
}
}
}
.padding()
}
private func createSet(count:Int) {
set.removeAll(keepingCapacity: true)
repeat {
let num = Int.random(in: 1...10000)
set.insert(num)
} while set.count < count
}
}
extension Int:Identifiable {
public var id:Self { self }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I made a break point on Text(String(num)). Every time the timer was trigger, the GridView updated. Why this happened? As the model of grid didn't change.
Updated
If I put the timer in another view, the grid view wouldn't be trigger.
import SwiftUI
struct ContentView: View {
#State private var set = Set<Int>()
#State private var count = "10"
private let columns:[GridItem] = Array(repeating: .init(.flexible()), count: 3)
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
HStack{
TextField("Create \(count) items", text: $count)
Button {
createSet(count: Int(count)!)
} label: {
Text("Create")
}
}
TimerView()
}
.padding()
}
private func createSet(count:Int) {
set.removeAll(keepingCapacity: true)
repeat {
let num = Int.random(in: 1...10000)
set.insert(num)
} while set.count < count
}
}
extension Int:Identifiable {
public var id:Self { self }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct TimerView: View {
#State private var timer:Timer? = nil
#State private var time = 0
var body: some View {
if let _ = timer {
Text(String(time))
.font(.title2)
.foregroundColor(.green)
}
HStack {
Button {
time = 100
let timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
time -= 10
if time == 0 {
self.timer?.invalidate()
self.timer = nil
}
}
self.timer = timer
} label: {
Text("Start Timer")
}
Button {
self.timer?.invalidate()
self.timer = nil
} label: {
Text("Stop Timer")
}
}
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
TimerView()
}
}
That´s pretty much how SwiftUI works. Every change to a #State var triggers the View to reevaluate. If you put your ForEach in another view it will only reevaluate if you change a var that changes that view. E.g. set or columns.
struct ExtractedView: View {
var columns: [GridItem]
var set: Set<Int>
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
}
}
It is encouraged in SwiftUI to make many small Views. The system driving this is pretty good in identifying what needs to be changed and what not. There is a very good WWDC video describing this.
WWDC
I'm trying to build an demo app by swiftUI that get multi text from user and add them to the list, below , there is an image of app every time user press plus button the AddListView show to the user and there user can add multi text to the List.I have a problem to add them to the list by new switUI data Flow I don't know how to pass data.(I comment more information)
Thanks 🙏
here is my code for AddListView:
import SwiftUI
struct AddListView: View {
#State var numberOfTextFiled = 1
#Binding var showAddListView : Bool
var body: some View {
ZStack {
Title(numberOfTextFiled: $numberOfTextFiled)
VStack {
ScrollView {
ForEach(0 ..< numberOfTextFiled, id: \.self) { item in
PreAddTextField()
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView)
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AddListView(showAddListView: .constant(false))
}
}
struct PreAddTextField: View {
// I made this standalone struct and use #State to every TextField text be independent
// if i use #Binding to pass data all Texfield have the same text value
#State var textInTextField = ""
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
// What should happen here to add Text to List???
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
#Binding var numberOfTextFiled : Int
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
numberOfTextFiled += 1
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
and for DataModel:
import SwiftUI
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
var textData = [
Text1(text: "SwiftUI"),
Text1(text: "Data flow?"),
]
and finally:
import SwiftUI
struct ListView: View {
#State var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Because of the multiple-items part of the question, this becomes a lot less trivial. However, using a combination of ObservableObjects and callback functions, definitely doable. Look at the inline comments in the code for explanations about what is going on:
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
//Store the items in an ObservableObject instead of just in #State
class AppState : ObservableObject {
#Published var textData : [Text1] = [.init(text: "Item 1"),.init(text: "Item 2")]
}
//This view model stores data about all of the new items that are going to be added
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "")] //start with one empty item
//save all of the new items -- don't save anything that is empty
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
//these Bindings get used for the TextFields -- they're attached to the item IDs
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.text ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, text: newValue)
}
}
}
}
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "")) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String //this takes a binding to the view model now
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void //callback function for what happens when "Add" gets pressed
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void //callback function for what happens when the plus button is hit
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
struct ListView: View {
#StateObject var appState = AppState() //store the AppState here
#State private var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(appState.textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView, appState: appState)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
I'm trying to create a List and allow only one item to be selected at a time. How would I do so in a ForEach loop? I can select multiple items just fine, but the end goal is to have only one checkmark in the selected item in the List. It may not even be the proper way to handle what I'm attempting.
struct ContentView: View {
var body: some View {
NavigationView {
List((1 ..< 4).indices, id: \.self) { index in
CheckmarkView(index: index)
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
//.environment(\.editMode, .constant(.active))
}
}
}
struct CheckmarkView: View {
let index: Int
#State var check: Bool = false
var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}
You'll need something to store all of the states instead of storing it per-checkmark view, because of the requirement to just have one thing checked at a time. I made a little example where the logic is handled in an ObservableObject and passed to the checkmark views through a custom Binding that handles checking/unchecking states:
struct CheckmarkModel {
var id = UUID()
var state = false
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.checkmarks[index].state
} set: { (newValue) in
self.checkmarks = self.checkmarks.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.state = newValue
} else {
//not the same index
if newValue {
itemCopy.state = false
}
}
return itemCopy
}
}
}
}
struct ContentView: View {
#ObservedObject var state = StateManager()
var body: some View {
NavigationView {
List(Array(state.checkmarks.enumerated()), id: \.1.id) { (index, item) in //<-- here
CheckmarkView(index: index + 1, check: state.singularBinding(forIndex: index))
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct CheckmarkView: View {
let index: Int
#Binding var check: Bool //<-- Here
var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}
What's happening:
There's a CheckmarkModel that has an ID for each checkbox, and the state of that box
StateManager keeps an array of those models. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the checkbox array. Any time a checkbox is set, it unchecks all of the other boxes. I also kept your original behavior of allowing nothing to be checked
The List now gets an enumeration of the state.checkmarks -- using enumerated lets me keep your previous behavior of being able to pass an index number to the checkbox view
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
List {
ForEach(0 ..< RemindTimeType.allCases.count) {
index in CheckmarkView(title:getListTitle(index), index: index, markIndex: $markIndex)
.padding(.all, 3)
}.listRowBackground(Color.clear)
}
struct CheckmarkView: View {
let title: String
let index: Int
#Binding var markIndex: Int
var body: some View {
Button(action: {
markIndex = index
}) {
HStack {
Text(title)
.foregroundColor(Color.white)
.font(.custom(FontEnum.Regular.fontName, size: 14))
Spacer()
if index == markIndex {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(hex: 0xe6c27c))
}
}
}
}
}
We can benefit from binding collections of Swift 5.5.
import SwiftUI
struct CheckmarkModel: Identifiable, Hashable {
var id = UUID()
var state = false
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
}
struct SingleSelectionList<Content: View>: View {
#Binding var items: [CheckmarkModel]
#Binding var selectedItem: CheckmarkModel?
var rowContent: (CheckmarkModel) -> Content
#State var previouslySelectedItemNdx: Int?
var body: some View {
List(Array($items.enumerated()), id: \.1.id) { (ndx, $item) in
rowContent(item)
.modifier(CheckmarkModifier(checked: item.id == self.selectedItem?.id))
.contentShape(Rectangle())
.onTapGesture {
if let prevIndex = previouslySelectedItemNdx {
items[prevIndex].state = false
}
self.selectedItem = item
item.state = true
previouslySelectedItemNdx = ndx
}
}
}
}
struct CheckmarkModifier: ViewModifier {
var checked: Bool = false
func body(content: Content) -> some View {
Group {
if checked {
ZStack(alignment: .trailing) {
content
Image(systemName: "checkmark")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.green)
.shadow(radius: 1)
}
} else {
content
}
}
}
}
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selectedItem: CheckmarkModel?
var body: some View {
VStack {
Text("Selected Item: \(selectedItem?.id.description ?? "Select one")")
Divider()
SingleSelectionList(items: $state.checkmarks, selectedItem: $selectedItem) { item in
HStack {
Text(item.id.description + " " + item.state.description)
Spacer()
}
}
}
}
}
A bit simplified version
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selection: CheckmarkModel.ID?
var body: some View {
List {
ForEach($state.checkmarks) { $item in
SelectionCell(item: $item, selectedItem: $selection)
.onTapGesture {
if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
state.checkmarks[ndx].state = false
}
selection = item.id
item.state = true
}
}
}
.listStyle(.plain)
}
}
struct SelectionCell: View {
#Binding var item: CheckmarkModel
#Binding var selectedItem: CheckmarkModel.ID?
var body: some View {
HStack {
Text(item.id.description + " " + item.state.description)
Spacer()
if item.id == selectedItem {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
}
A version that uses internal List's selected mark and selection:
import SwiftUI
struct CheckmarkModel: Identifiable, Hashable {
var name: String
var state: Bool = false
var id = UUID()
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(name: "Name1"), CheckmarkModel(name: "Name2"), CheckmarkModel(name: "Name3"), CheckmarkModel(name: "Name4")]
}
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selection: CheckmarkModel.ID?
#State private var selectedItems = [CheckmarkModel]()
var body: some View {
VStack {
Text("Items")
List($state.checkmarks, selection: $selection) { $item in
Text(item.name + " " + item.state.description)
}
.onChange(of: selection) { s in
for index in state.checkmarks.indices {
if state.checkmarks[index].state == true {
state.checkmarks[index].state = false
}
}
selectedItems = []
if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
state.checkmarks[ndx].state = true
selectedItems = [state.checkmarks[ndx]]
print(selectedItems)
}
}
.environment(\.editMode, .constant(.active))
Divider()
List(selectedItems) {
Text($0.name + " " + $0.state.description)
}
}
Text("\(selectedItems.count) selections")
}
}
I'm trying to change the SwiftUI list to update after tapping the checkbox button in my list.
When I tap on a list row checkbox button, I call a function to set the immediate rows as checked which ID is less then the selected one. I could modify the ArrayList as selected = 0 to selected = 1. But as my list is Published var it should emit the change to my list view. but it doesn't.
Here's what I've done:
// ViewModel
import Foundation
import Combine
class BillingViewModel: ObservableObject {
#Published var invoiceList = [InvoiceModel](){
didSet {
self.didChange.send(true)
}
}
#Published var shouldShow = true
let didChange = PassthroughSubject<Bool, Never>()
init() {
setValues()
}
func setValues() {
for i in 0...10 {
invoiceList.append(InvoiceModel(ispID: 100+i, selected: 0, invoiceNo: "Invoice No: \(100+i)"))
}
}
func getCombinedBalance(ispID: Int) {
DispatchQueue.main.async {
if let row = self.invoiceList.firstIndex(where: {$0.ispID == ispID}) {
self.changeSelection(row: row)
}
}
}
func changeSelection(row: Int) {
if invoiceList[row].selected == 0 {
let selectedRows = invoiceList.map({ $0.ispID ?? 0 <= invoiceList[row].ispID ?? 0 })
print(selectedRows)
for index in 0..<invoiceList.count {
if selectedRows[index] {
invoiceList[index].selected = 1
} else {
invoiceList[index].selected = 0
}
}
} else {
let selectedRows = invoiceList.map({ $0.ispID ?? 0 <= invoiceList[row].ispID ?? 0 })
print(selectedRows)
for index in 0..<invoiceList.count {
if selectedRows[index] {
invoiceList[index].selected = 1
} else {
invoiceList[index].selected = 0
}
}
}
}
}
// List View
import SwiftUI
struct ContentView: View {
#StateObject var billingViewModel = BillingViewModel()
#State var shouldShow = true
var body: some View {
NavigationView {
ZStack {
VStack {
List {
ForEach(billingViewModel.invoiceList) { invoice in
NavigationLink(
destination: InvoiceDetailsView(billingViewModel: billingViewModel)) {
invoiceRowView(billingViewModel: billingViewModel, invoice: invoice)
}
}
}
}
}.navigationBarTitle(Text("Invoice List"))
}
}
}
// Invoice Row View
import SwiftUI
struct invoiceRowView: View {
#StateObject var billingViewModel: BillingViewModel
#State var invoice: InvoiceModel
var body: some View {
VStack(alignment: .leading) {
HStack {
Button(action: {
if invoice.selected == 0 {
print(invoice)
billingViewModel.getCombinedBalance(ispID: invoice.ispID ?? 0)
} else {
print(invoice)
billingViewModel.getCombinedBalance(ispID: invoice.ispID ?? 0)
}
}, label: {
Image(systemName: invoice.selected == 0 ? "checkmark.circle" : "checkmark.circle.fill")
.resizable()
.frame(width: 32, height: 32, alignment: .center)
}).buttonStyle(PlainButtonStyle())
Text(invoice.invoiceNo ?? "Hello, World!").padding()
Spacer()
}
}
}
}
// Data Model
import Foundation
struct InvoiceModel: Codable, Identifiable {
var id = UUID()
var ispID: Int?
var selected: Int?
var invoiceNo: String?
}
You need to use binding instead of externally injected state (which is set just to copy of value), i.e.
struct invoiceRowView: View {
#StateObject var billingViewModel: BillingViewModel
#Binding var invoice: InvoiceModel
// .. other code
and thus inject binding in parent view
ForEach(billingViewModel.invoiceList.indices, id: \.self) { index in
NavigationLink(
destination: InvoiceDetailsView(billingViewModel: billingViewModel)) {
invoiceRowView(billingViewModel: billingViewModel, invoice: $billingViewModel.invoiceList[index])
}
}
I took an example from this question: How does one enable selections in SwiftUI's List and edited the code to be able to delete rows one by one. But I don't know how to delete multiple rows from list.
Could you help me, please?
var demoData = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "Wanda Green"]
struct ContentView : View {
#State var selectKeeper = Set<String>()
var body: some View {
NavigationView {
List(selection: $selectKeeper){
ForEach(demoData, id: \.self) { name in
Text(name)
}
.onDelete(perform: delete)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))
}
}
func delete(at offsets: IndexSet) {
demoData.remove(atOffsets: offsets)
}
}
solution from SwiftUI how to perform action when EditMode changes?
struct Item: Identifiable {
let id = UUID()
let title: String
static var i = 0
init() {
self.title = "\(Item.i)"
Item.i += 1
}
}
struct ContentView: View {
#State var editMode: EditMode = .inactive
#State var selection = Set<UUID>()
#State var items = [Item(), Item(), Item()]
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items) { item in
Text(item.title)
}
}
.navigationBarTitle(Text("Demo"))
.navigationBarItems(
leading: editButton,
trailing: addDelButton
)
.environment(\.editMode, self.$editMode)
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var addDelButton: some View {
if editMode == .inactive {
return Button(action: addItem) {
Image(systemName: "plus")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
private func addItem() {
items.append(Item())
}
private func deleteItems() {
for id in selection {
if let index = items.lastIndex(where: { $0.id == id }) {
items.remove(at: index)
}
}
selection = Set<UUID>()
}
}
extension EditMode {
var title: String {
self == .active ? "Done" : "Edit"
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}