SwiftUI: Change Binding's Value from Button's Action - swiftui

I have a cart item detail view with the "Add To Order" button. I want to increase the number of items (cartItem.count) when the button is tapped and wonder why this code doesn't update the UI (Text("(cartItem.count)")):
import SwiftUI
struct CartItemDetail: View {
#Binding var cartItem: CartItem
var body: some View {
VStack {
Text("\(cartItem.count)")
Text(cartItem.product.name)
.font(.largeTitle)
cartItem.product.image
.resizable()
.frame(width: 200, height: 200).clipShape(Circle())
Text("\(cartItem.product.price) | \(cartItem.product.calories)")
Text(cartItem.product.description)
.multilineTextAlignment(.center)
.padding(.all, 20.0)
Button(action: {
cartItem.count = cartItem.count + 1
})
{
RoundedButton(imageName: "cart.badge.plus", text: "Add to order")
}
}
}}
Some more code (if needed to reproduce)
struct ContentView: View {
#ObservedObject var cart: Cart
var body: some View {
NavigationView {
List($cart.cartItems) { $cartItem in
NavigationLink(destination: CartItemDetail(cartItem: $cartItem)
) {
CartItemRow(cartItem: $cartItem)
}
}
}.navigationTitle("Cart")
}}
Cart and CartItem's code:
class Cart: ObservableObject {
#Published var cartItems: [CartItem]
init() {
self.cartItems = []
for product in testDataProducts {
cartItems.append(CartItem(product: product, count: 1))
}
}}
class CartItem: Identifiable {
var id: String
var product: Product
var count: Int
init(product: Product, count: Int) {
self.id = UUID().uuidString
self.product = product
self.count = count
}}

Related

SwiftUI - Subtotal TextField entries across multiple views

I have multiple views created by a ForEACH. Each View has a textfield where a user can enter a number. I would like to subtotal each entry in each view. In other words subtotal the binding in each view.
Is my approach wrong?
ForEach(someArray.allCases, id: \.id) { item in
CustomeRowView(name: item.rawValue)
}
struct CustomeRowView: View {
var name: String
#State private var amount: String = ""
var body: some View {
VStack {
HStack {
Label(name, systemImage: image)
VStack {
TextField("Amount", text: $amount)
.frame(width: UIScreen.main.bounds.width / 7)
}
}
}
}
}
Any help would be greatly appreciated.
there are many ways to achieve what you ask. I present here a very
simple approach, using an ObservableObject to keep the info in one place.
It has a function to add to the info dictionary fruits.
A #StateObject is created in ContentView to keep one single source of truth.
It is passed to the CustomeRowView view using #ObservedObject, and used to tally
the input of the TextField when the return key is pressed (.onSubmit).
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class FruitCake: ObservableObject {
#Published var fruits: [String : Int] = ["apples":0,"oranges":0,"bananas":0]
// adjust for you purpose
func add(to name: String, amount: Int) {
if let k = fruits.keys.first(where: {$0 == name}),
let sum = fruits[k] {
fruits[k] = sum + amount
}
}
}
struct ContentView: View {
#StateObject var fruitCake = FruitCake()
var body: some View {
VStack {
ForEach(Array(fruitCake.fruits.keys), id: \.self) { item in
CustomeRowView(name: item, fruitCake: fruitCake)
}
}
}
}
struct CustomeRowView: View {
let name: String
#ObservedObject var fruitCake: FruitCake
#State private var amount = 0
var body: some View {
HStack {
Label(name, systemImage: "info")
TextField("Amount", value: $amount, format: .number)
.frame(width: UIScreen.main.bounds.width / 7)
.border(.red)
.onSubmit {
fruitCake.add(to: name, amount: amount)
}
// subtotal
Text("\(fruitCake.fruits[name] ?? 0)")
}
}
}

How to set up Textfield and Button in Custom SwiftUI View

I am attempting the configure the text field and button in my openweathermap app to be in its own view other than the main content view. In TextFieldView, the action of the button is set up to call an API response. Then, the weather data from the response is populated on a sheet-based DetailView, which is triggered by the button in TextFieldView. I configured the ForEach method in the sheet to return the last city added to the WeatherModel array (which would technically be the most recent city entered into the text field), then populate the sheet-based DetailView with weather data for that city. Previously, When I had the HStack containing the text field, button, and sheet control set up in the ContentView, the Sheet would properly display weather for the city that had just entered into the text field. After moving those items to a separate TextFieldView, the ForEach method appears to have stopped working. Instead, the weather info returned after entering a city name into the text field is displayed on the wrong count. For instance, if I were to enter "London" in the text field, the DetailView in the sheet is completely blank. If I then enter "Rome" as the next entry, the DetailView in the sheet shows weather info for the previous "London" entry. Entering "Paris" in the textfield displays weather info for "Rome", and so on...
To summarize, the ForEach method in the sheet stopped working properly after I moved the whole textfield and button feature to a separate view. Any idea why the issue I described is happening?
Here is my code:
ContentView
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
List {
ForEach(viewModel.cityNameList.reversed()) { city in
NavigationLink(destination: DetailView(detail: city), label: {
Text(city.name).font(.system(size: 18))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°")
.font(.system(size: 18))
})
}
.onDelete { indexSet in
let reversed = Array(viewModel.cityNameList.reversed())
let items = Set(indexSet.map { reversed[$0].id })
viewModel.cityNameList.removeAll { items.contains($0.id) }
}
}
.refreshable {
viewModel.updatedAll()
}
TextFieldView(viewModel: viewModel)
}.navigationBarTitle("Weather", displayMode: .inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
TextFieldView
struct TextFieldView: View {
#State private var cityName = ""
#State private var showingDetail = false
#FocusState var isInputActive: Bool
var viewModel: WeatherViewModel
var body: some View {
HStack {
TextField("Enter City Name", text: $cityName)
.focused($isInputActive)
Spacer()
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Done") {
isInputActive = false
}
}
}
if isInputActive == false {
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
Image(systemName: "plus")
.font(.largeTitle)
.frame(width: 75, height: 75)
.foregroundColor(Color.white)
.background(Color(.systemBlue))
.clipShape(Circle())
}
.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
.frame(minWidth: 100, idealWidth: 150, maxWidth: 500, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
.padding(.leading, 16)
.padding(.trailing, 16)
}
}
struct TextFieldView_Previews: PreviewProvider {
static var previews: some View {
TextFieldView(viewModel: WeatherViewModel())
}
}
DetailView
struct DetailView: View {
#State private var cityName = ""
#State var selection: Int? = nil
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName.escaped())&units=imperial&appid=<YourAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.addToList(model)
}
}
catch {
print(error)
}
}
task.resume()
}
func updatedAll() {
// keep a copy of all the cities names
let listOfNames = cityNameList.map{$0.name}
// fetch the up-to-date weather info
for city in listOfNames {
fetchWeather(for: city)
}
}
func addToList( _ city: WeatherModel) {
// if already have this city, just update
if let ndx = cityNameList.firstIndex(where: {$0.name == city.name}) {
cityNameList[ndx].main = city.main
cityNameList[ndx].weather = city.weather
} else {
// add a new city
cityNameList.append(city)
}
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
var humidity = 0
}
struct WeatherInfo: Codable {
var description: String = ""
}
You need to use an ObservedObject in your TextFieldView to use your
original (single source of truth) #StateObject var viewModel that you create in ContentView and observe any change to it.
So use this:
struct TextFieldView: View {
#ObservedObject var viewModel: WeatherViewModel
...
}

How to update Navigation Link subscript with an array of views inside a ForEach Loop?

I am utilizing a search bar from a Kavsoft Tutorial here: "https://www.youtube.com/watch?v=nuag1PILxCA&t=14s", I'm wondering on how to add navigation links to each of the items, I decided on embedding the itemView inside a navigation link with an array of views to loop through but it seems that it doesn't accept the index as a parameter giving "Cannot convert value of type 'item' to expected argument type 'Int'", instead I incremented the subscript on appear in the navigation link, although that updates the variable, but it doesn't seem to work for the different views themselves only navigating to the first view.
I've linked all the code needed to reproduce the problem but due to my incredibly limited experience in reproducing the problem in as less code as possible, I am not able to do so. Below the main issue of concern is the block starting from the VStack. Starting the program can be done by just adding Search_Bar() to content view body.
struct Home: View {
let views : [AnyView] = [ AnyView(untitled_Skull()), AnyView(dogs()), AnyView(cats()) ]
#Binding var filteredItems : [item]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
var i = 0
VStack(spacing: 15){
ForEach(filteredItems){index in
NavigationLink(destination: views[i]
) {
itemView(item: index)
}.onAppear() {
i = i + 1
}
}
}
.padding()
}
}
}
func add(value: Int) -> Int {
let value = value + 1
return value
}
struct itemView: View {
var item: item
#State var show = false
var body: some View {
HStack(spacing: 15){
VStack {
let colorArray: [Color] = [.yellowLichtenstien, .redHaring, .orangeBasquiat, .pinkWarhol]
HStack {
Text(item.name)
.foregroundColor(.white)
.bold()
.padding(.leading)
Spacer()
}
HStack {
Text(item.subText)
.bold()
.foregroundColor (.white)
.font(.subheadline)
.padding(.leading)
Circle()
.frame(width: 5, height: 5)
.foregroundColor(colorArray[item.color])
Text(item.subText2)
.bold()
.foregroundColor (.white)
.font(.subheadline)
Spacer()
}
Spacer()
}
}
.padding(.horizontal)
}
}
struct item: Identifiable {
var id = UUID().uuidString
// both Image And Name Are Same....
var name: String
// since all Are Apple Native Apps...
var color: Int
var subText: String
var subText2: String
}
var searchItems = [
item(name: "Untitled (Skull)", color: 0, subText: "1983", subText2: "yay"),
item(name: "Dogs", color: 1, subText: "1972", subText2: "wow"),
item(name: "Cats", color: 2, subText: "1968", subText2: "oof")
]
struct Search_Bar: View {
#State var filteredItems = searchItems
var body: some View {
CustomNavigationView(view: AnyView(Home(filteredItems: $filteredItems)), placeHolder: "Museums, Art or anything else.", largeTitle: true, title: "Search",
onSearch: { (txt) in
if txt != ""{
self.filteredItems = searchItems.filter{$0.name.lowercased().contains(txt.lowercased())}
}
else{
self.filteredItems = searchItems
}
}, onCancel: {
// Do Your Own Code When Search And Canceled....
self.filteredItems = searchItems
})
.ignoresSafeArea()
}
}
struct Search_Bar_Previews: PreviewProvider {
static var previews: some View {
Search_Bar()
}
}
import SwiftUI
struct CustomNavigationView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return CustomNavigationView.Coordinator(parent: self)
}
// Just Change Your View That Requires Search Bar...
var view: AnyView
// Ease Of Use.....
var largeTitle: Bool
var title: String
var placeHolder: String
// onSearch And OnCancel Closures....
var onSearch: (String)->()
var onCancel: ()->()
// requre closure on Call...
init(view: AnyView,placeHolder: String? = "Search",largeTitle: Bool? = true,title: String,onSearch: #escaping (String)->(),onCancel: #escaping ()->()) {
self.title = title
self.largeTitle = largeTitle!
self.placeHolder = placeHolder!
self.view = view
self.onSearch = onSearch
self.onCancel = onCancel
}
// Integrating UIKit Navigation Controller With SwiftUI View...
func makeUIViewController(context: Context) -> UINavigationController {
// requires SwiftUI View...
let childView = UIHostingController(rootView: view)
let controller = UINavigationController(rootViewController: childView)
// Nav Bar Data...
controller.navigationBar.topItem?.title = title
controller.navigationBar.prefersLargeTitles = largeTitle
// search Bar....
let searchController = UISearchController()
searchController.searchBar.placeholder = placeHolder
// setting delegate...
searchController.searchBar.delegate = context.coordinator
// setting Search Bar In NavBar...
// disabling hide on scroll...
// disabling dim bg..
searchController.obscuresBackgroundDuringPresentation = false
controller.navigationBar.topItem?.hidesSearchBarWhenScrolling = false
controller.navigationBar.topItem?.searchController = searchController
return controller
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
// Updating Real Time...
uiViewController.navigationBar.topItem?.title = title
uiViewController.navigationBar.topItem?.searchController?.searchBar.placeholder = placeHolder
uiViewController.navigationBar.prefersLargeTitles = largeTitle
}
// search Bar Delegate...
class Coordinator: NSObject,UISearchBarDelegate{
var parent: CustomNavigationView
init(parent: CustomNavigationView) {
self.parent = parent
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// when text changes....
self.parent.onSearch(searchText)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
// when cancel button is clicked...
self.parent.onCancel()
}
}
}
Letting the random view below for the array being for example:
import SwiftUI
struct cats: View {
var body: some View {
Text("cats") //replacing this with dogs or untitled skull as an example.
}
}
struct cats_Previews: PreviewProvider {
static var previews: some View {
cats()
}
}
You can use ForEach getting the item and its index in the closure :
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: views[index]){
Text(item.name)
}
}
For example :
struct ListItem: Identifiable {
let id = UUID()
let name: String
}
struct SwiftUIView17: View {
#State private var filteredItems = ["John", "Bob", "Maria"].map(ListItem.init)
let views = [AnyView(Text("John destination")), AnyView(Text("Bob destination")), AnyView(Text("Maria destination"))]
var body: some View {
ScrollView {
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: views[index]){
Text(item.name)
}
}
}
}
}
But it would be better not to use AnyView but a ViewBuilder :
struct SwiftUIView17: View {
#State private var filteredItems = ["John", "Bob", "Maria"].map(ListItem.init)
#ViewBuilder func destination(for itemIndex: Int) -> some View {
switch itemIndex {
case 0: Text("John destination")
case 1: Text("Bob destination").foregroundColor(.red)
case 2: Rectangle()
default: Text("error")
}
}
var body: some View {
ScrollView {
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: destination(for: index)){
Text(item.name)
}
}
}
}
}

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

SwiftUI update CustomView on enum variable change

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