With SwiftUI / Combine, How to avoid to put cancellables in ViewModel - mvvm

I have always placed the DisposeBag in ViewController in MVVM with RxSwift like it said in this topic:
On iOS, for the DisposeBag in MVVM, can it be placed in ViewModel?
But with combine, since the View is a struct and cancelable can't be placed in this, I am stuck with solution.
How to manage subscription between View and VM in Combine without add cancelable in ViewModel
Or maybe, in SwiftUI / Combine, there is no choice to place cancelables in VM.
There is an example of implementation in SiwftUI / Combine :
The ViewModel
class EquityViewModel: ObservableObject {
#Injected private var api: AlphaVantageAPI
private var cancellables = Set<AnyCancellable>()
private let code: String
#Published private var result: Quote?
#Published var price: String = ""
init(code: String) {
self.code = code
self.$result
.map {
return "\($0?.price ?? 0) €"
}.assign(to: &$price)
}
func addToPortfolio(){
}
func onAppear() {
self.api.quote(symbol: self.code).share()
.sink { completion in }
receiveValue: { quote in
self.result = quote.quote
}
.store(in: &cancellables)
}
}
The View
struct EquityView: View {
#ObservedObject var viewModel: EquityViewModel
init(viewModel: EquityViewModel) {
self.viewModel = viewModel
}
var body: some View {
ZStack {
Color("primary").edgesIgnoringSafeArea(.all)
VStack {
Text("Stock Price")
.foregroundColor(.white)
.frame(minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading)
.padding()
HStack {
Text(self.viewModel.price)
.foregroundColor(.white)
Text("+4.75 %")
.foregroundColor(.white)
.padding(.leading, 20)
}.frame(minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading)
.padding()
Button(action: self.viewModel.addToPortfolio, label: {
Text("Add to portfolio")
.foregroundColor(.white)
.frame(minWidth: 0,
maxWidth: .infinity,
maxHeight: 30,
alignment: .center)
.background(Color.blue)
.cornerRadius(5)
}).padding()
Spacer()
}
}.frame(alignment: .leading)
.onAppear(perform: self.viewModel.onAppear)
}
}

It that what you search? I can't test it i don't know what a library you are use
class EquityViewMode: ObservableObject {
// #Injected private var api: AlphaVantageAPI
var pricePublisher: AnyPublisher<String, Never>
#Published var price: String = ""
init(){
// init your publisher like
// pricePublisher = self.api.quote(symbol: self.code)
// .share()
// .map { "\($0?.price ?? 0) €" }
// .eraseToAnyPublisher()
}
struct EquityView: View {
#ObservedObject var viewModel: EquityViewModel
var handle: AnyCancellable? = nil
init(m:EquityViewMode) {
viewModel = m
handle = m.pricePublisher.assign(to: \.price, on: self.viewModel)
}
var body: some View{
Text(viewModel.price)
}
}

You have to store them in some place (generally in the viewModel) to have the reference and delete them when you don't need them anymore. To do it you can use the deinit method:
deinit {
cancellables.removeAll()
}

Related

How to setup NavigationLink in SwiftUI sheet to redirect to new view

I am attempting to build a multifaceted openweathermap app. My app is designed to prompt the user to input a city name on a WelcomeView, in order to get weather data for that city. After clicking search, the user is redirected to a sheet with destination: DetailView, which displays weather details about that requested city. My goal is to disable dismissal of the sheet in WelcomeView and instead add a navigationlink to the sheet that redirects to the ContentView. The ContentView in turn is set up to display a list of the user's recent searches (also in the form of navigation links).
My issues are the following:
The navigationLink in the WelcomeView sheet does not work. It appears to be disabled. How can I configure the navigationLink to segue to destination: ContentView() ?
After clicking the navigationLink and redirecting to ContentView, I want to ensure that the city name entered in the WelcomeView textfield is rendered as a list item in the ContentView. For that to work, would it be necessary to set up an action in NavigationLink to call viewModel.fetchWeather(for: cityName)?
Here is my code:
WelcomeView
struct WelcomeView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State private var showingDetail: Bool = false
#State private var linkActive: Bool = true
#State private var acceptedTerms = false
var body: some View {
Section {
HStack {
TextField("Search Weather by City", text: $cityName)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10.0).strokeBorder(Color.gray, style: StrokeStyle(lineWidth: 1.0)))
.padding()
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}
.sheet(isPresented: $showingDetail) {
VStack {
NavigationLink(destination: ContentView()){
Text("Return to Search")
}
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}.interactiveDismissDisabled(!acceptedTerms)
}
}
}.padding()
}
}
}
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
}
}
ContentView
let coloredToolbarAppearance = UIToolbarAppearance()
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State var showingDetail = false
init() {
// toolbar attributes
coloredToolbarAppearance.configureWithOpaqueBackground()
coloredToolbarAppearance.backgroundColor = .systemGray5
UIToolbar.appearance().standardAppearance = coloredToolbarAppearance
UIToolbar.appearance().scrollEdgeAppearance = coloredToolbarAppearance
}
var body: some View {
NavigationView {
VStack() {
List () {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°").font(.system(size: 32))
}
}
}.onDelete { index in
self.viewModel.cityNameList.remove(atOffsets: index)
}
}.onAppear() {
viewModel.fetchWeather(for: cityName)
}
}.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DetailView
struct DetailView: View {
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)&units=imperial&appid=<MyAPIKey>") 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.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}
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
}
struct WeatherInfo: Codable {
var description: String = ""
}
DemoApp
#main
struct SwftUIMVVMWeatherDemoApp: App {
var body: some Scene {
WindowGroup {
// ContentView()
WelcomeView()
}
}
}

Get a specific id in a modal

I'm still learning on the job and my question may seem stupid.
I've got a list of movies and on the tap I want to show card of the selected movie.
So I've got my ResultsView
var results:[DiscoverResult]
#State private var resultsCount:Int = 0
#State private var isPresented:Bool = false
#EnvironmentObject private var genres:Genres
var body: some View {
ScrollView {
ForEach (results){ result in
Button(action: {
isPresented.toggle()
}, label: {
ZStack {
ZStack {
KFImage(URL (string: baseUrlForThumb + result.posterPath)).resizable().scaledToFill()
.frame( height: 150)
.mask(Rectangle().frame( height: 150))
Rectangle().foregroundColor(.clear) // Making rectangle transparent
.background(LinearGradient(gradient: Gradient(colors: [.clear, .clear, .black]), startPoint: .top, endPoint: .bottom))
}.frame( height: 150)
// Titre du film
VStack(alignment: .center) {
Spacer()
Text(result.title)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
// Genres du film
Text(genres.generateGenresList(genreIDS: result.genreIDS)).font(.caption).foregroundColor(.white).multilineTextAlignment(.center)
} .padding()
}.padding(.horizontal)
})
.sheet(isPresented: $isPresented, content: {
MovieView(isPresented: $isPresented, movieId: result.id)
})
.navigationTitle(result.title)
}
}
}
}
And my MovieView
import SwiftUI
struct MovieView: View {
#Binding var isPresented: Bool
var movieId:Int
var body: some View {
VStack {
Text(String(movieId))
.padding()
Button("Fermer") {
isPresented = false
}
}
}
}
But the movie card still the same even list element selected.
I think that the 'result.id' is overwrite at every loop but i don't know how to fix it.
Sorry for my english mistakes.
thank for your purpose.
Instead of using isPresented for .sheet you can use .sheet(item:, content:) and pass the whole result object
.sheet(item: $selecteditem( { result in
MovieView(item: result)
}
To make this work you need a new property (you can remove isPresented)
#State private var selectedItem: DiscoverResult?
and you need update your MovieView struct
struct MovieView: View {
let result: DiscoverResult
var body: some View {
//...
}
}
or pass only the movie id to your MovieView if you prefer that.

How do I pass a var from one viewModel to a new viewModel in my .fullScreenCover() View?

I'm building an app with swiftUI & firebase/firestore. I open a fullscreenCover sheet of a product selected from a catalog and the user can add it to a list below it. The product is selected on the previous page, and that is passed onto the .fullScreeenCover, where I'm introducing a new ViewModel for the list.
Where I'm getting confused: How do I pass the product ID passed into this fullScreenCover view into my newly introduced list's viewModel so that I can run the "add to list" function?
ViewModel for my List:
import SwiftUI
import Firebase
class ListViewModel: ObservableObject {
let var product: Product
#Published var userList = [List]()
#Published var list: List
init(list: List) {
self.list = list
}
func fetchList() {
let docRef = Firestore.firestore().collection("List")
guard let uid = AuthViewModel.shared.userSession?.uid else { return }
docRef.whereField("uid", isEqualTo: uid).getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.userList = documents.map({ List(dictionary: $0.data())} )}
}
func AddProductToList(product: Product, list: List) {
let listRef = Firestore.firestore().collection("List").document(list.id).collection("Products")
let productRef = Firestore.firestore().collection("Products").document(product.id)
productRef.getDocument { snapshot, _ in
listRef.document(self.product.id).setData([:]) { _ in
print("\(self.product.title) was saved to \(self.list.name)")
}
}
}
}
Code for .fullScreenCover() sheet View
import SwiftUI
struct ListCoverView: View {
#Binding var isPresented: Bool
let viewModel: LikeViewModel
#StateObject var listViewModel = ListViewModel(list)
var body: some View {
ZStack {
VStack (spacing: 10) {
VStack {
WebImage(url: URL(string: viewModel.product.image))
.resizable()
.frame(width: 220, height: 220)
.padding(.top,10)
Text("\(viewModel.product.title)")
.fontWeight(.bold)
.foregroundColor(.black)
.padding(.horizontal)
Text(viewModel.product.company)
.foregroundColor(.black)
.fontWeight(.bold)
}
.onAppear(perform: {
// Fetch products
listViewModel.fetchList()
})
ScrollView {
VStack {
Button(action: listViewModel.AddProductToList(Product)) {
ForEach(listViewModel.userList){list in
ListRow(list: listViewModel.list, viewModel: listViewModel)
}
}
}
}
}
}
Spacer()
Button(action: {isPresented.toggle()}, label : {
Text("Close")
.font(.system(size: 20))
.foregroundColor(.black)
})
.padding()
}
}
Parent View of the .fullScreenCover()
import SwiftUI
import SDWebImageSwiftUI
struct CardView: View {
let product: Product
#ObservedObject var viewModel: LikeViewModel
#State private var isShowingNewListSheet = false
init(product: Product) {
self.product = product
self.viewModel = LikeViewModel(product: product)
}
var body: some View {
VStack {
WebImage(url: URL(string: product.image))
.resizable()
.aspectRatio(contentMode: .fit)
Text(product.title)
.fontWeight(.bold)
.foregroundColor(.black)
.padding(.horizontal)
Text(product.company)
.foregroundColor(.black)
.fontWeight(.semibold)
.padding(.trailing,65)
HStack{
Button(action: {
viewModel.didLike ? viewModel.UnlikeProduct() : viewModel.LikeProduct()
}, label : {
Image(systemName: viewModel.didLike ? "heart.fill" : "heart")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(viewModel.didLike ? .red : .black)
})
.padding(.trailing,5)
Button(action: { isShowingNewListSheet.toggle()
}, label : {
Image(systemName: "square.and.arrow.down")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.black)
})
.fullScreenCover(isPresented: $isShowingNewListSheet) {
ListCoverView(isPresented: $isShowingNewListSheet, viewModel: viewModel)
}
}
}
.padding(.bottom)
.background(Color(.white))
.cornerRadius(15)
}
}

Why My second view cannot jump back to the root view properly

My App currently has two pages, first page has a circle plus button which could lead us to a second page. Basically, I have a save button which after clicking it, we could get back to the rood page. I followed this link for going back to root view. I tried the most up voted code, his code works perfectly. I reduced his code to two scene (basically the same scenario as mine), which also works perfectly. But then I don't know why my own code, pasted below, doesn't work. Basically my way of handling going back to root view is the same as the one in the link.
//
// ContentView.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/22/20.
//
import SwiftUI
import UIKit
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
struct ContentView: View {
#EnvironmentObject private var fridge : Fridge
private var dbStartWith=0;
#State var pushed: Bool = false
#State private var selection = 1;
#State private var addFood = false;
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(1)
Text("random tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("profile")
}
.tag(0)
}.environmentObject(fridge)
}
}
struct FoodView: View{
var body: some View{
NavigationView{
Text("food destination view ");
}
}
}
struct AddFoodView: View{
#Binding var pushed : Bool
#EnvironmentObject private var fridgeView : Fridge
#State private var name = ""
#State private var count : Int = 1
#State private var category : String = "肉类";
#State var showCategory = false
#State var showCount = false
var someNumberProxy: Binding<String> {
Binding<String>(
get: { String(format: "%d", Int(self.count)) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.count = value.intValue;
}
}
)
}
var body: some View{
ZStack{
NavigationView{
VStack{
Button (action: {
self.pushed = false ;
//let tempFood=Food(id: fridgeView.index!,name: name, count: count, category: category);
//fridgeView.addFood(food: tempFood);
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
ZStack{
if self.showCount{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCount=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $count,label: EmptyView()) {
ForEach(1..<100){ number in
Text("\(number)").tag("\(number)")
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
.offset(x:10,y:-10)
Spacer()
}
if self.showCategory{
let categoryArr = ["肉类","蔬菜类","饮料类","调味品类"]
ZStack{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCategory=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $category,label: EmptyView()) {
ForEach(0..<categoryArr.count){ number in
Text(categoryArr[number]).tag(categoryArr[number])
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
Spacer()
}.offset(x:10,y:20)
}
}
}.animation(.easeInOut)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you read my code carefully, there are some variables are missing referencing. That's because I pasted part of the code that relates to my issue.
Food Class
//
// Food.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/23/20.
//
import Foundation
class Food: Identifiable {
init(id:Int, name: String, count: Int, category: String){
self.id=id;
self.name=name;
self.count=count;
self.category=category;
}
var id: Int
var name: String
var count: Int
var category: String
}
Fridge class
//
// Fridge.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/27/20.
//
import Foundation
class Fridge: ObservableObject{
init(){
db=DBhelper();
let result = setIndex(database: db!);
self.index = result.1;
self.container=result.0;
}
var db:DBhelper?
var index : Int?
#Published var container : [Food]?;
func setIndex(database: DBhelper) -> ([Food],Int){
let foodList : [Food] = database.read();
var index=0;
for food in foodList{
index = max(food.id,index);
}
return (foodList,(index+1));
}
func updateindex(index: inout Int){
index=index+1;
}
func testExist(){
if let data = db {
print("hi")
}
else{
print("doesnt exist")
}
}
func addFood(food:Food){
self.db!.insert(id: self.index!, name: food.name, count:food.count, category: food.category);
self.container!.append(food);
}
}
Because you implemented a new NaviagtionView in AddFoodView. Simply remove this and it should work. Look at the link you provided. There is no NavigationView in the child.
Correct me if Im wrong but the core code parts here that produce this issue are as follows:
Here you start:
struct ContentView: View {
#State var pushed: Bool = false
// Deleted other vars
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
// Here you navigate to the child view
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
Here you land and want to go back to root:
struct AddFoodView: View{
#Binding var pushed : Bool
// Deleted the other vars for better view
var body: some View{
ZStack{
NavigationView{ // <-- remove this
VStack{
Button (action: {
// here you'd like to go back
self.pushed = false;
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
For the future:
I have the feeling you might have troubles with the navigation in general.
Actually it is really simple:
You implement one NavigationView at the "root" / start of your navigation.
From there on you only use NavigationLinks to go further down to child pages. No NavigationView needed anymore.

Background of button not conditionally rendering

Essentially I have a button when pressed I want the background to become a different color. In order to do this I have an object that I alter, I have printed out the value of the Bool value in the object and see its changing but the color of the button is not changing.
Object With Bool:
class dummyObject: Identifiable, ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var id = UUID()
var isSelected: Bool {
willSet {
objectWillChange.send()
}
}
init(isSelected:Bool) {
self.isSelected = isSelected
}
}
View:
struct SelectionView: View {
var objs: [dummyObject] = [
dummyObject.init(isSelected: false)
]
var body: some View {
HStack{
ForEach(objs) { obj in
Button(action: {
obj.isSelected.toggle()
print("\(obj.isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(obj.isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(obj.isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}.frame(minWidth: 0, maxWidth: .infinity)
.padding()
}
}
Extract your Button into other view, where obj is #ObservedObject and everything will work:
import SwiftUI
import Combine
class dummyObject: Identifiable, ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var id = UUID()
var isSelected: Bool {
willSet {
objectWillChange.send()
}
}
init(isSelected:Bool) {
self.isSelected = isSelected
}
}
struct SelectionView: View {
var objs: [dummyObject] = [dummyObject.init(isSelected: false)]
var body: some View {
HStack{
ForEach(objs) { obj in
ObjectButton(obj: obj)
}
}
}
}
struct ObjectButton: View {
#ObservedObject var obj: dummyObject
var body: some View {
Button(action: {
self.obj.isSelected.toggle()
print("\(self.obj.isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(obj.isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(obj.isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}
struct SelectionView_Previews: PreviewProvider {
static var previews: some View {
SelectionView()
}
}
Here is modified your snapshot of code that works. Tested with Xcode 11.2 / iOS 13.2.
The main idea is made a model as value-type, so modifications of properties modify model itself, and introducing #State for view would refresh on changes.
struct dummyObject: Identifiable, Hashable {
var id = UUID()
var isSelected: Bool
}
struct SelectionView: View {
#State var objs: [dummyObject] = [
dummyObject(isSelected: false)
]
var body: some View {
HStack{
ForEach(Array(objs.enumerated()), id: \.element) { (i, _) in
Button(action: {
self.objs[i].isSelected.toggle()
print("\(self.objs[i].isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(self.objs[i].isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(self.objs[i].isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}.frame(minWidth: 0, maxWidth: .infinity)
.padding()
}
}