SwiftUI update CustomView on enum variable change - swiftui

I have months view on grid 3x4. Each subview is a small box with month name and state, selected/unselected. The problem is... I want to observe enum variable from parent view and deselect all buttons except the last pressed.
For now I had next logic implemented. Initially I have currentMonthSelected with state .none (no months selected). When I press JAN button, i pass currentMonthSelected == .jax to Single month subview and it returns me back callback that change currentMonthSelected which should observe other views.
ParentView
#State var currentMonthSelected: MonthsTypes = .none
SingleButtonView(title: .jan, isSelected: currentMonthSelected == .jan ? true : false, action: { month in
self.currentMonthSelected = month
})
SingleButtonView(title: .feb, isSelected: currentMonthSelected == .feb ? true : false, action: { month in
self.currentMonthSelected = month
})
Single month subview
struct SingleButtonView: View {
var title: MonthsTypes = .none
#State var isSelected = false
var action: (MonthsTypes) -> ()
var body: some View {
VStack(spacing: 0){
Button(action: {
self.action(self.title)
}){
Spacer()
Text(title.rawValue.prefix(3))
.font(.Montserrat(weight: isSelected ? .SemiBold : .Regular, size: 16))
.foregroundColor(isSelected ? Color.white : Color.gray)
Spacer()
}
}
.frame(width: 80, height: 40)
.background(isSelected ? Color.white : Color.brand_purple)
}
}

enum MonthsTypes: String {
case jan = "January"
case feb = "February"
case none
}
struct ContentView: View {
#State var currentMonthSelected: MonthsTypes = .none
var body: some View {
VStack() {
SingleButtonView(title: .jan, currentMonthSelected: $currentMonthSelected)
SingleButtonView(title: .feb, currentMonthSelected: $currentMonthSelected)}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SingleButtonView: View {
var title: MonthsTypes = .none
#Binding var currentMonthSelected: MonthsTypes
var isSelected: Bool {
if title == currentMonthSelected { return true}
return false
}
var body: some View {
VStack(spacing: 0){
Button(action: {
self.currentMonthSelected = self.title
}){
Spacer()
Text(title.rawValue.prefix(3))
.foregroundColor(isSelected ? Color.white : Color.gray)
Spacer()
}
}
.frame(width: 80, height: 40)
.background(isSelected ? Color.white : Color.yellow)
}
}

Such view hierarchy is much easier to manage via one view model. Here is a demo of approach based on your code (a just drop some lines with custom fonts/colors, which however does not affect idea).
struct ParentView_Previews: PreviewProvider {
static var previews: some View {
ParentView().environmentObject(ViewModel()) // inject view model
}
}
enum MonthsTypes: String { // restored enum
case none = "None"
case jan = "January"
case feb = "February"
case mar = "March"
}
class ViewModel: ObservableObject { // selection holder (extendable for anything)
#Published var currentMonthSelected: MonthsTypes = .none
}
struct ParentView: View {
#EnvironmentObject var viewModel: ViewModel // assuming injected by .environmentObject(ViewModel)
var body: some View {
VStack {
SingleButtonView(title: .jan) // simple declaration, action can be added
SingleButtonView(title: .feb)
SingleButtonView(title: .mar)
}
}
}
struct SingleButtonView: View {
#EnvironmentObject var vm: ViewModel // logic on selection change is inside
var title: MonthsTypes = .none
var action: (MonthsTypes) -> () = { _ in } // default action does nothing
var body: some View {
VStack(spacing: 0){
Button(action: {
// change selection to self or toggle
self.vm.currentMonthSelected = (self.vm.currentMonthSelected != self.title ? self.title : MonthsTypes.none)
self.action(self.title) // callback if needed
}){
Spacer()
Text(title.rawValue.prefix(3))
.foregroundColor(self.vm.currentMonthSelected == self.title ? Color.white : Color.gray)
Spacer()
}
}
.frame(width: 80, height: 40)
.background(vm.currentMonthSelected == title ? Color.purple : Color.white)
}
}

Related

FocusState Textfield not working within toolbar ToolbarItem

Let me explain, I have a parent view with a SearchBarView, im passing down a focus state binding like this .
SearchBarView(searchText:$object.searchQuery, searching: $object.searching, focused: _searchIsFocused
That works perfectly as #FocusState var searchIsFocused: Bool is defined in parent view passing it down to the SearchBarView (child view ). In parent I can check the change in value and everything ok.
The problem relies when in parent I have the SearchBarView inside .toolbar {} and ToolBarItem(). nothing happens, not change in value of focus, etc. I have my SearchBarView in the top navigation bar and still want to use it there.. but I need to be able to know when it is in focus. if I use inside any VStack or whatever, everything perfectly..
-- EDIT --
providing more code to test
SearchBarView
struct SearchBarView: View {
#Environment(\.colorScheme) var colorScheme
#Binding var searchText: String
#Binding var searching: Bool
#FocusState var focused: Bool
var body: some View {
ZStack {
Rectangle()
.foregroundColor(colorScheme == .dark ? Color("darkSearchColor") : Color.white)
.overlay(
RoundedRectangle(cornerRadius: 13)
.stroke(.black.opacity(0.25), lineWidth: 1)
)
HStack {
Image(systemName: "magnifyingglass").foregroundColor( colorScheme == .dark ? .gray : .gray )
TextField("Search..", text: $searchText )
.focused($focused, equals: true)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20))
.disableAutocorrection(true).onSubmit {
let _ = print("Search textfield Submited by return button")
}
}
.foregroundColor(colorScheme == .dark ? Color("defaultGray") :.gray)
.padding(.leading, 13)
.padding(.trailing, 20).overlay(
HStack {
Spacer()
if searching {
ActivityIndicator().frame(width:15,height:15).aspectRatio(contentMode: .fit).padding(.trailing,15)
}
}
)
.onChange(of: focused) { searchIsFocused in
let _ = print("SEARCH IS FOCUSED VALUE: \(searchIsFocused) ")
}
}
.frame(height: 36)
.cornerRadius(13)
}
}
-- home View Code --
struct HomeView: View {
#Environment(\.colorScheme) var colorScheme
#FocusState var searchIsFocused: Bool
#State var searching:Bool = false
#State var searchQuery: String = ""
var body: some View {
NavigationStack {
GeometryReader { geofull in
ZStack(alignment: .bottom) {
Color("background")//.edgesIgnoringSafeArea([.all])
ScrollView(showsIndicators: false) {
VStack {
// Testing Bar inside VStack.. Here It Works. comment the bar the leave
// the one inside the .toolbar ToolbarItem to test
SearchBarView(searchText:$searchQuery, searching: $searching, focused: _searchIsFocused).padding(0)
}.toolbar {
//MARK: Navbar search field
ToolbarItem(placement:.principal) {
SearchBarView(searchText:$searchQuery, searching: $searching, focused: _searchIsFocused).padding(0)
}
}
.onChange(of: searchIsFocused) { searchIsFocused in
let _ = print("HOME VIEW searchIsFocused VALUE: \(searchIsFocused) ")
}
}
}
}
}
}
}

How to Add multi text into the list in SwiftUI?(Data Flow)

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)
}
)
}
}
}

SwiftUI List single selectable item

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")
}
}

Keeping instances unique inside ForEach in SwiftUI

I have a view that includes a ForEach, and I have a button that adds more items to the list. In each instance, in the loop, I have a TextField that is pre-filled with an autogenerated counter name. But when I add a new instance, all the previously added items change the name to the same:
What I would like is to have the counter names be Counter 1 for the first, the second Counter 2, third Counter 3, etc.
Here's my code:
import SwiftUI
struct Counter: Identifiable, Equatable {
var id: UUID = UUID()
var name: String = ""
var rows: Int = 0
var repeats: Int?
var rowsPerRepeat: Int?
var countRepeats: Bool = false
}
struct Project: Identifiable, Equatable {
var id:UUID = UUID()
var name:String = ""
var counters: [Counter] = [Counter]()
}
class AddEditCounterViewModel : ObservableObject {
#Published var counter : Counter
#Published var project: Project
init(counter: Counter, project: Project) {
self.project = project
self.counter = counter
if self.counter.name.isEmpty {
self.counter.name = counterNameGenerator()
}
}
func counterNameGenerator() -> String {
let count = project.counters.count
return String.localizedStringWithFormat(NSLocalizedString("Counter %d", comment: "Counter name"), count)
}
func countRepeats(countRepeats : Bool) {
if countRepeats {
counter.countRepeats = true
if counter.rowsPerRepeat == nil {
counter.rowsPerRepeat = 2
}
} else {
counter.countRepeats = false
counter.rowsPerRepeat = nil
}
}
}
struct AddEditCounterView: View {
#ObservedObject var viewModel : AddEditCounterViewModel
#State var countRepeats = false
#State var hiddenHeight : CGFloat = 0.0
#State var opacity = 0.0
init(viewModel: AddEditCounterViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack(spacing: 20) {
VStack (alignment: .leading) {
Text("Counter Name")
.multilineTextAlignment(.leading)
TextField("", text: $viewModel.counter.name)
}
HStack {
Text("Start at")
.multilineTextAlignment(.leading)
Spacer()
TextField("", value: $viewModel.counter.rows, formatter: NumberFormatter())
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.frame(width: 70.0, height: nil, alignment: .leading)
}.frame(maxWidth: .infinity, alignment: .leading)
Toggle(isOn: $countRepeats) {
Text("Count sets (repeats)")
}.onChange(of: countRepeats, perform: { value in
viewModel.countRepeats(countRepeats: value)
hiddenHeight = value ? 30.0 : 0
opacity = value ? 1 : 0
})
HStack {
Text("How many rows per set (repeat)")
.multilineTextAlignment(.leading)
Spacer()
TextField("", value: $viewModel.counter.rowsPerRepeat, formatter: NumberFormatter())
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.frame(width: 70.0, height: nil, alignment: .leading)
}
.opacity(opacity)
.frame(maxWidth: .infinity, maxHeight: $hiddenHeight.wrappedValue, alignment: .leading)
.animation(.easeIn)
}
}
}
class AddEditProjectViewModel: ObservableObject {
#Published var project : Project
init(project: Project) {
self.project = project
if self.project.counters.count < 1 {
addNewCounter()
}
}
func addNewCounter() {
project.counters.append(Counter())
}
}
struct AddEditProjectView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
var body: some View {
VStack {
ForEach(viewModel.project.counters) { counter in
AddEditCounterView(viewModel: AddEditCounterViewModel(counter: viewModel.project.counters[0], project: viewModel.project))
}
Button( action: {
viewModel.addNewCounter()
}){
Text("Add Counter")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
AddEditProjectView(viewModel: AddEditProjectViewModel(project: Project()))
}
}

How to Transmit a View Entry Count to a Class Method

I'm having trouble with usage of a count of the number of entries in a view. I especially need to know when there are no entries in the view. I have placed debug code in the view below and the view count currants.curItem.countis updating as expected. The count status in checkForUpdates() doesn't follow the view above.
If I recall correctly I should be using #EnvironmentObject or #ObservedObject only in a view. I really need some kind of global variable that I can pass to the method checkForUpdates. It is crashing when count in checkForUpdates() is nonzero when in the view it is actually zero. It also crashes in checkForUpdates() with the error Fatal error: No ObservableObject of type Currencies found. A View.environmentObject(_:) for Currencies may be missing as an ancestor of this view.
struct manCurView: View {
#EnvironmentObject var currants: Currants
var body: some View {
List {
ForEach(currants.curItem, id: \.id) { item in
HStack {
Text(item.curCode)
.frame(width: 100, alignment: .center)
Text(item.cunName)
}
.font(.subheadline)
}
.onDelete(perform: removeItems)
}
.navigationBarTitle(Text("Manage Working Blocks"), displayMode: .inline)
HStack {
NavigationLink(destination: addCurView()) {Text("Add Working Blocks").fontWeight(.bold)}
.font(.title2)
.disabled(currants.curItem.count > 7)
Here is how the data is stored for the view above
struct CurItem: Codable, Identifiable {
var id = UUID()
var cunName: String
var curName: String
var curCode: String
var curSymbol: String
var curRate: Double
}
class Currants: ObservableObject {
#Published var curItem: [CurItem]
}
And here is the class and method where I would like to use count from the view manCurView
class BlockStatus: ObservableObject {
#EnvironmentObject var globalCur : Currants
#ObservedObject var netStatus : TestNetStatus = TestNetStatus()
func checkForUpdates() -> (Bool) {
if netStatus.connected == true {
if globalCur.curItem.count > 0 {
Without a minimal reproducible example it is very difficult to give you exact code but you can try something like the code below in your manCurView
#StateObject var blockStatus: BlockStatus = BlockStatus()
.onChange(of: currants.curItem.count, perform: { value in
print("send value from here")
blockStatus.arrayCount = value
})
And adding the code below to BlockStatus
#Published var arrayCount: Int = 0{
didSet{
//Call your method here
}
}
Look at the code below.
import SwiftUI
import Combine
struct CurItem: Codable, Identifiable {
var id = UUID()
}
class Currants: ObservableObject {
#Published var curItem: [CurItem] = [CurItem(), CurItem(), CurItem(), CurItem()]
}
class TestNetStatus: ObservableObject {
static let sharedInstance = TestNetStatus()
#Published var connected: Bool = false
init() {
//Simulate changes in connection
Timer.scheduledTimer(withTimeInterval: 10, repeats: true){ timer in
self.connected.toggle()
}
}
}
class BlockStatus: ObservableObject {
#Published var arrayCount: Int = 0{
didSet{
checkForUpdates()
}
}
#Published var checkedForUpdates: Bool = false
var netStatus : TestNetStatus = TestNetStatus.sharedInstance
//private var cancellable: AnyCancellable?
init() {
//Maybe? if you want to check upon init.
//checkForUpdates()
//Something like the code below is also possible but with 2 observed objects the other variable could be outdated
// cancellable = netStatus.objectWillChange.sink { [weak self] in
// self?.checkForUpdates()
// }
}
func checkForUpdates() {
if netStatus.connected == true {
if arrayCount > 0 {
checkedForUpdates = true
}else{
checkedForUpdates = false
}
}else{
checkedForUpdates = false
}
}
}
struct ManCurView: View {
#StateObject var currants: Currants = Currants()
#StateObject var blockStatus: BlockStatus = BlockStatus()
#StateObject var testNetStatus: TestNetStatus = TestNetStatus.sharedInstance
var body: some View {
List {
Text("checkedForUpdates = " + blockStatus.checkedForUpdates.description).foregroundColor(blockStatus.checkedForUpdates ? Color.green : Color.red)
Text("connected = " + blockStatus.netStatus.connected.description).foregroundColor(blockStatus.netStatus.connected ? Color.green : Color.red)
ForEach(currants.curItem, id: \.id) { item in
HStack {
Text(item.id.uuidString)
.frame(width: 100, alignment: .center)
Text(item.id.uuidString)
}
.font(.subheadline)
}
//Replaced with toolbar button for sample
//.onDelete(perform: removeItems)
//When the array count changes
.onChange(of: currants.curItem.count, perform: { value in
blockStatus.arrayCount = value
})
//Check when the networkStatus changes
.onChange(of: testNetStatus.connected, perform: { value in
//Check arrayCount
if blockStatus.arrayCount != currants.curItem.count{
blockStatus.arrayCount = currants.curItem.count
}else{
blockStatus.checkForUpdates()
}
})
}
.navigationBarTitle(Text("Manage Working Blocks"), displayMode: .inline)
//Replaced addCurView call with toolbar button for sample
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing, content: {
Button("add-currant", action: {
currants.curItem.append(CurItem())
})
})
ToolbarItem(placement: .navigationBarLeading, content: {
Button("delete-currant", action: {
if currants.curItem.count > 0{
currants.curItem.removeFirst()
}
})
})
})
}
}
Here is ContentView: Notice in the menu that because this is a view I can use count directly to disable entry input. Down in getData() notice that I'm calling blockStatus.checkForUpdates() to determine if is OK to call the API. A fault will occur if currants.curItem.count = 0
I just realized that technically getData() is part of the ContentView so I can change the call below to if blockStatus.checkForUpdates() == true && currants.curItem.count != 0 {
I'm going to spend some time studying your suggestions above to see if I could use this in the future.
So thanks for all the help by looking into this. I wasn't aware of the suggestions on code displayed on Stackoverflow. I'll be sure to follow those guidelines in the future. Galen
import SwiftUI
import CoreData
import Combine
struct ContentView: View {
#EnvironmentObject var userData: UserData
#EnvironmentObject var currants: Currants
#EnvironmentObject var blockStatus: BlockStatus
var body: some View {
NavigationView {
VStack (alignment: .center) {
Text("Title")
.font(.title)
.fontWeight(.bold)
Spacer()
Group {
NavigationLink(destination: entryView()) {Text("Entry")}
.disabled(currants.curItem.count == 0)
Spacer()
NavigationLink(destination: totalView()) {Text("View Totals")}
Spacer()
NavigationLink(destination: listView()) {Text("View Entries")}
Spacer()
NavigationLink(destination: xchView()) {Text("View Dates")}
}
Rectangle()
.frame(height: 130)
.foregroundColor(Color.white)
}
.font(.title2)
.navigationBarItems(leading: NavigationLink (destination: settingsView()) {
Image(systemName: "gear")
.foregroundColor(.gray)
.font(.system(.title3))
}, trailing: NavigationLink( destination: aboutView()) {
Text("About")
})
.onAppear(perform: getData)
}
}
func getData() {
// check criteria for updating data once daily
if blockStatus.checkForUpdates() == true {
print(" doing update")
---- API HERE -----
}.resume()
}
}
}