SwiftUI List not updating after deleting row from Sqlite DB - swift

I have a list of objects that a user can click on to navigate to a detailed view and then delete. This works fine but when I added a .swipeActions() to the cardListRow I get an index out of bounds error after deleting.
Initial View:
struct ContentView: View {
//variables for Ingredient list:
#State var ingredients: [Ingredient] = []
var body: some View {
NavigationView{
ScrollView{
VStack{
//Ingredient Section
VStack{
List{
ForEach(self.$ingredients, id: \.id){ ingredientModel in
//print each ingredient
CardListRow(item: ingredientModel)
.listRowSeparator(.hidden)
}
}
.frame(height: 180)
.listStyle(.plain)
.onAppear(perform: {
print("Load ingredients from DB")
self.ingredients = Ingredient_DB().getIngredients()
})
}
}
}
}
}
CardListRow:
struct CardListRow: View {
#Binding var item: Ingredient
#State var inStock: Bool = false
#State var showingAlert: Bool = false
var body: some View {
ZStack {
Color.white
.cornerRadius(12)
IngredientListItem(ingredient: $item)
}
.fixedSize(horizontal: false, vertical: true)
.shadow(color: Color.black.opacity(0.2), radius: 3, x: 0, y: 2)
.swipeActions() {
if self.inStock == true {
Button (action: {
self.inStock = false
item.inStock = self.inStock
Ingredient_DB().updateIngredient(idValue: self.item.id.uuidString, nameValue: self.item.name, inStockValue: self.item.inStock)
}) {
Text("Out of stock")
}
.tint(.yellow)
}else{
Button (action: {
self.inStock = true
item.inStock = self.inStock
Ingredient_DB().updateIngredient(idValue: self.self.item.id.uuidString, nameValue: self.item.name, inStockValue: self.item.inStock)
}) {
Text("In stock")
}
.tint(.green)
}
}
.onAppear(perform: {
self.inStock = item.inStock //Error occurs here. List isn't reloaded but item is out of index
})
}
}
IngredientListItem:
struct IngredientListItem: View {
//ingredient to display
#Binding var ingredient: Ingredient
//to see if ingredient was clicked on
#State var ingredientSelected: Bool = false
var body: some View {
//navigation link to view ingredient info
NavigationLink (destination: ViewIngredientView(ingredient: $ingredient), isActive: self.$ingredientSelected){
EmptyView()
}
HStack {
if !ingredient.inStock{
Image(systemName: "x.square")
.foregroundColor(.red)
.padding(.leading, 5)
}
Text(ingredient.name)
.font(.body)
.padding(.leading, 5)
.frame(minWidth: 100)
Divider()
.frame(width: 10)
Spacer()
}
.padding(.top, 3)
.padding(.bottom, 3)
}
}
ViewIngredientView:
struct ViewIngredientView: View {
//Name of recipe received from previous view
#Binding var ingredient: Ingredient
//To return to previous view
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
VStack {
Text(ingredient.name)
.font(.title)
.padding(.leading, 5)
}
.navigationBarItems(trailing:
HStack{
Spacer()
//Delete Button
Button("Delete") {
//Remove recipe from Recipe_DB
Ingredient_DB().deleteIngredient(ingredientID: ingredient.id.uuidString)
//Remove recipe from Recipe_Ingredient_DB
Recipe_Ingredient_DB().deleteIngredient(ingredientIDValue: ingredient.id.uuidString)
//return to previous screen
self.mode.wrappedValue.dismiss()
}
.padding()
})
}
}
Ingredient:
import Foundation
class Ingredient: Identifiable, Hashable{
static func == (lhs: Ingredient, rhs: Ingredient) -> Bool {
if (lhs.id == rhs.id) {return true}
else {return false}
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
public var id = UUID()
public var name: String = ""
public var inStock: Bool = false
}

Related

SwiftUI Popup window

I created my first simple card game.
https://codewithchris.com/first-swiftui-app-tutorial/
Now I want to add a pop up window which will pop up with the message "You win" if the player gets 15 points and "You lose" if the CPU gets 15 points first.
Can someone please help me how to do it?
I would be glad if there is some tutorial so I can do it myself, not just copy and paste it.
import SwiftUI
struct ContentView: View {
#State private var playCard = "card5"
#State private var cpuCard = "card9"
#State private var playerScore = 0
#State private var cpuScore = 0
var body: some View {
ZStack {
Image("background-plain")
.resizable()
.ignoresSafeArea()
VStack{
Spacer()
Image("logo")
HStack{
Spacer()
Image(playCard)
Spacer()
Image(cpuCard)
Spacer()
}
Button(action: {
//reset
playerScore = 0
cpuScore = 0
}, label: {
Image(systemName: "clock.arrow.circlepath")
.font(.system(size: 60))
.foregroundColor(Color(.systemRed)) })
Button(action: {
//gen. random betw. 2 and 14
let playerRand = Int.random(in: 2...14)
let cpuRand = Int.random(in: 2...14)
//Update the cards
playCard = "card" + String(playerRand)
cpuCard = "card" + String(cpuRand)
//Update the score
if playerRand > cpuRand {
playerScore += 1
}
else if cpuRand > playerRand {
cpuScore += 1
}
}, label: {
Image("button")
})
HStack{
Spacer()
VStack{
Text("Player")
.font(.headline)
.padding(.bottom, 10.0)
Text(String(playerScore))
.font(.largeTitle)
}
Spacer()
VStack{
Text("CPU")
.font(.headline)
.padding(.bottom, 10.0)
Text(String(cpuScore))
.font(.largeTitle)
}
Spacer()
}
.foregroundColor(.white)
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Make your popup like a view. And after that. Call it in method present a view.
struct ContentView : View {
#State var showingPopup = false // 1
var body: some View {
ZStack {
Color.red.opacity(0.2)
Button("Push me") {
showingPopup = true // 2
}
}
.popup(isPresented: $showingPopup) { // 3
ZStack { // 4
Color.blue.frame(width: 200, height: 100)
Text("Popup!")
}
}
}
}
extension View {
public func popup<PopupContent: View>(
isPresented: Binding<Bool>,
view: #escaping () -> PopupContent) -> some View {
self.modifier(
Popup(
isPresented: isPresented,
view: view)
)
}
}
public struct Popup<PopupContent>: ViewModifier where PopupContent: View {
init(isPresented: Binding<Bool>,
view: #escaping () -> PopupContent) {
self._isPresented = isPresented
self.view = view
}
/// Controls if the sheet should be presented or not
#Binding var isPresented: Bool
/// The content to present
var view: () -> PopupContent
}

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

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.

SwiftUI How to create LazyVStack with selection as List

I would like to have something like List(selection: ) in LazyVStack.
The problem is that I don't know how to manage the content to split in each element that it contains.
What I've tried to do:
public struct LazyVStackSelectionable<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
let content: Content
var selection: Binding<Set<SelectionValue>>?
#Environment(\.editMode) var editMode
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content) {
self.content = content()
self.selection = selection
}
public var body: some View {
if self.editMode?.wrappedValue == EditMode.active {
HStack {
content //here I would like to have something like ForEach (content, id:\.self)
Button(action: {
//add the UUID to the list of selected item
}) {
Image(systemName: "checkmark.circle.fill")
//Image(systemName: selection?.wrappedValue.contains(<#T##member: Hashable##Hashable#>) ? "checkmark.circle.fill" : "circle")
}
}
}
else {
content
}
}
}
struct ListView: View {
#State private var editMode: EditMode = .inactive
#State private var selection = Set<UUID>()
#State private var allElements: [MyElement] = [MyElement(id: UUID(), text: "one"),
MyElement(id: UUID(), text: "two" ),
MyElement(id: UUID(), text: "tree" )
]
var body: some View {
NavigationView {
VStack {
Divider()
Text("LazyVStack")
.foregroundColor(.red)
LazyVStack {
ForEach(allElements, id: \.self) { element in //section data
Text(element.text)
}
}
Divider()
Text("LazyVStackSelectionable")
.foregroundColor(.red)
LazyVStackSelectionable(selection: $selection) {
ForEach(allElements, id: \.self) { element in //section data
Text(element.text)
}
}
Divider()
}
.environment(\.editMode, self.$editMode)
.navigationBarTitle(Text("LIST"), displayMode: .inline)
.navigationBarItems(//EDIT
trailing:
Group {
HStack (spacing: 15) {
self.editButton
self.delInfoButton
.contentShape(Rectangle())
}
}
)
}
}
//MARK: EDIT MODE
private func deleteItems() {
DispatchQueue.global(qos: .userInteractive).async {
Thread.current.name = #function
selection.forEach{ idToRemove in
if let index = allElements.firstIndex(where: { $0.id == idToRemove }) {
allElements.remove(at: index)
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var delInfoButton: some View {
if editMode == .inactive {
return Button(action: {}) {
Image(systemName: "square.and.arrow.up")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
edit = .inactive
edit = .active
UPDATE
with Asperi's solution, I lose the propriety of LazyVStack, all the rows are loaded also if not displayed (and is also not scrollable:
struct SampleRow: View {
let number: Int
var body: some View {
Text("Sel Row \(number)")
}
init(_ number: Int) {
print("Loading LazySampleRow row \(number)")
self.number = number
}
}
struct LazySampleRow: View {
let number: Int
var body: some View {
Text("LVS element \(number)")
}
init(_ number: Int) {
print("Loading LazyVStack row \(number)")
self.number = number
}
}
var aLotOfElements: [MyElement] {
var temp: [MyElement] = []
for i in 1..<200 {
temp.append(MyElement(id: UUID(), number: i))
}
return temp
}
struct ContentView: View {
#State private var editMode: EditMode = .inactive
#State private var selection = Set<UUID>()
#State private var allElements: [MyElement] = aLotOfElements//[MyElement(id: UUID(), number: 1)]
var body: some View {
NavigationView {
HStack {
VStack {
Text("LazyVStack")
.foregroundColor(.red)
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(allElements, id: \.self) { element in //section data
LazySampleRow(element.number)
}
}
}
}
Divider()
VStack {
LazyVStack (alignment: .leading) {
Divider()
Text("LazyVStackSelectionable")
.foregroundColor(.red)
LazyVStackSelectionable(allElements, selection: $selection) { element in
SampleRow(element.number)
}
Divider()
}
}
}
.environment(\.editMode, self.$editMode)
.navigationBarTitle(Text("LIST"), displayMode: .inline)
.navigationBarItems(//EDIT
trailing:
Group {
HStack (spacing: 15) {
self.editButton
self.delInfoButton
.contentShape(Rectangle())
}
}
)
}
}
//MARK: EDIT MODE
private func deleteItems() {
DispatchQueue.global(qos: .userInteractive).async {
Thread.current.name = #function
selection.forEach{ idToRemove in
if let index = allElements.firstIndex(where: { $0.id == idToRemove }) {
allElements.remove(at: index)
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var delInfoButton: some View {
if editMode == .inactive {
return Button(action: {}) {
Image(systemName: "square.and.arrow.up")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension EditMode {
var title: String {
self == .active ? NSLocalizedString("done", comment: "") : NSLocalizedString("edit", comment: "")
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}
You need to create custom handled containers for all variants of desired content types.
Below is a demo of possible direction on the example of following content support (by example of List)
LazyVStackSelectionable(allElements, selection: $selection) { element in
Text(element.text)
}
Demo prepared and tested with Xcode 12 / iOS 14 (it is used some SwiftUI 2.0 features so if needed SwiftUI 1.0 support some more tuning will be needed)
struct LazyVStackSelectionable<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
#Environment(\.editMode) var editMode
private var selection: Binding<Set<SelectionValue>>?
private var content: () -> Content
private var editingView: AnyView?
init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: #escaping () -> Content)
{
self.selection = selection
self.content = content
}
var body: some View {
Group {
if editingView != nil && self.editMode?.wrappedValue == .active {
editingView!
} else {
self.content()
}}
}
}
extension LazyVStackSelectionable {
init<Data, RowContent>(_ data: Data, selection: Binding<Set<SelectionValue>>?, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable, SelectionValue == Data.Element.ID
{
self.init(selection: selection, content: {
ForEach(data) { el in
HStack {
rowContent(el)
}
}
})
editingView = AnyView(
ForEach(data) { el in
HStack {
rowContent(el)
if let selection = selection {
Button(action: {
if selection.wrappedValue.contains(el.id) {
selection.wrappedValue.remove(el.id)
} else {
selection.wrappedValue.insert(el.id)
}
}) {
Image(systemName: selection.wrappedValue.contains(el.id) ? "checkmark.circle.fill" : "circle")
}
}
}
}
)
}
}
Instead of creating custom LazyVStack I suggest to modify ContentView and pass bindings to it.
struct SampleRow: View {
let element: MyElement
let editMode: Binding<EditMode>
let selection: Binding<Set<UUID>>?
var body: some View {
HStack {
if editMode.wrappedValue == .active,
let selection = selection {
Button(action: {
if selection.wrappedValue.contains(element.id) {
selection.wrappedValue.remove(element.id)
} else {
selection.wrappedValue.insert(element.id)
}
}) {
Image(systemName: selection.wrappedValue.contains(element.id) ? "checkmark.circle.fill" : "circle")
}
}
Text("Sel Row \(element.number)")
}
}
init(_ element: MyElement,
editMode: Binding<EditMode>,
selection: Binding<Set<UUID>>?) {
print("Loading LazySampleRow row \(element.number)")
self.editMode = editMode
self.element = element
self.selection = selection
}
}
And then you can just wrap normal LazyVStack in ScrollView to achieve what you need.
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(allElements, id: \.self) {
SampleRow($0,
editMode: $editMode,
selection: $selection)
}
}
}

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