if Array is empty, append all values in enum to array - ios

First Dabble in SwiftUI, I've managed to get the below code working such that when I press a button, it will show a "selected" state and add the selected sports into an array. (and remove from the array if "deselected")
However, I can't figure out how to initialise the sportsArray with ALL values within the enum HelperIntervalsIcu.icuActivityType.allCases if it is initially empty.
I tried to put in
if sportsArray.isEmpty {
HelperIntervalsIcu.icuActivityType.allCases.forEach {
sportsArray.append($0.rawvalue)
}
but Xcode keeps telling me type() cannot conform to View or things along those lines
struct selectSports: View {
#State private var sportsArray = [String]()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
// https://stackoverflow.com/a/69455949/14414215
ForEach(Array(HelperIntervalsIcu.icuActivityType.allCases), id:\.rawValue) { sport in
Button(action: {
addSports(sport: sport.rawValue)
}) {
HStack {
Image(getSportsIcon(sport: sport.rawValue))
.selectedSportsImageStyle(sportsArray: sportsArray, sport: sport.rawValue)
Text(sport.rawValue)
}
}
.buttonStyle(SelectedSportButtonStyle(sportsArray: sportsArray, sport: sport.rawValue))
}
}
}
}
struct SelectedSportButtonStyle: ButtonStyle {
var sportsArray: [String]
var sport: String
var selectedSport : Bool {
if sportsArray.contains(sport) {
return true
} else {
return false
}
}
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(selectedSport ? Font.subheadline.bold() : Font.subheadline)
.aspectRatio(contentMode: .fit)
.foregroundColor(selectedSport ? Color.orange : Color(UIColor.label))
.padding([.leading, .trailing], 15)
.padding([.top, .bottom],10)
.overlay(
RoundedRectangle(cornerRadius: 5.0)
.stroke(lineWidth: 2.0)
.foregroundColor(selectedSport ? Color.orange : Color.gray)
)
.offset(x: 10, y: 0)
}
}
func addSports(sport: String) {
if sportsArray.contains(sport) {
let sportsIndex = sportsArray.firstIndex(where: { $0 == sport })
sportsArray.remove(at: sportsIndex!)
} else {
sportsArray.append(sport)
}
print("Selected Sports:\(sportsArray)")
}
}
No Sports Selected (in this case sportsArray is empty and thus the default state which I would like have would be to have ALL sports Pre-selected)
2 Sports Selected

I assume you wanted to do that in onAppear, like
struct selectSports: View {
#State private var sportsArray = [String]()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
// .. other code
}
.onAppear {
if sportsArray.isEmpty { // << here !!
HelperIntervalsIcu.icuActivityType.allCases.forEach {
sportsArray.append($0.rawvalue)
}
}
}
}
}

Related

Swiftui view doesn't refresh when navigated to from a different view

I have, what is probably, a beginner question here. I'm hoping there is something simple I'm missing or I have done wrong.
I essentially have a view which holds a struct containing an array of id strings. I then have a #FirestoreQuery which accesses a collection which holds objects with these id's. My view then displays a list with two sections. One for the id's in the original struct, and one for the remaining ones in the collection which don't appear in the array.
Each listitem is a separate view which displays the details of that item and also includes a button. When this button is pressed it adds/removes that object from the parent list and the view should update to show that object in the opposite section of the list from before.
My issue is that this works fine in the 'preview' in xcode when I look at this view on it's own. However if I run the app in the simulator, or even preview a parent view and navigate to this one, the refreshing of the view doesn't seem to work. I can press the buttons, and nothing happens. If i leave the view and come back, everything appears where it should.
I'll include all the files below. Is there something I'm missing here?
Thanks
Main view displaying the list with two sections
import SwiftUI
import FirebaseFirestoreSwift
struct SessionInvitesView: View {
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#Binding var sessionViewModel : TrainingSessionViewModel
#State private var searchText: String = ""
#State var refresh : Bool = false
var enrolledClients : [Client] {
return clients.filter { sessionViewModel.session.invites.contains($0.id!) }
}
var availableClients : [Client] {
return clients.filter { !sessionViewModel.session.invites.contains($0.id!) }
}
var searchFilteredClients : [Client] {
if searchText.isEmpty {
return availableClients
} else {
return availableClients.filter {
$0.dogName.localizedCaseInsensitiveContains(searchText) ||
$0.name.localizedCaseInsensitiveContains(searchText) ||
$0.dogBreed.localizedCaseInsensitiveContains(searchText) }
}
}
var backButton: some View {
Button(action: { self.onCancel() }) {
Text("Back")
}
}
var body: some View {
NavigationView {
List {
Section(header: Text("Enrolled")) {
ForEach(enrolledClients) { client in
SessionInviteListItem(client: client, isEnrolled: true, onTap: removeClient)
}
}
Section(header: Text("Others")) {
ForEach(searchFilteredClients) { client in
SessionInviteListItem(client: client, isEnrolled: false, onTap: addClient)
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $searchText)
.navigationTitle("Invites")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: backButton)
}
}
func removeClient(clientId: String) {
self.sessionViewModel.session.invites.removeAll(where: { $0 == clientId })
refresh.toggle()
}
func addClient(clientId: String) {
self.sessionViewModel.session.invites.append(clientId)
refresh.toggle()
}
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
}
struct SessionInvitesView_Previews: PreviewProvider {
#State static var model = TrainingSessionViewModel()
static var previews: some View {
SessionInvitesView(sessionViewModel: $model)
}
}
List item view
import SwiftUI
struct SessionInviteListItem: View {
var client : Client
#State var isEnrolled : Bool
var onTap : (String) -> ()
var body: some View {
HStack {
VStack(alignment: .leading) {
HStack {
Text(client.dogName.uppercased())
.bold()
Text("(\(client.dogBreed))")
}
Text(client.name)
.font(.subheadline)
}
Spacer()
Button(action: { onTap(client.id!) }) {
Image(systemName: self.isEnrolled ? "xmark.circle.fill" : "plus.circle.fill")
}
.buttonStyle(.borderless)
.foregroundColor(self.isEnrolled ? .red : .green)
}
}
}
struct SessionInviteListItem_Previews: PreviewProvider {
static func doNothing(_ : String) {}
static var previews: some View {
SessionInviteListItem(client: buildSampleClient(), isEnrolled: false, onTap: doNothing)
}
}
Higher level view used to navigate to this list view
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionEditView: View {
// MARK: - Member Variables
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#StateObject var sheetManager = SheetManager()
var mode: Mode = .new
var dateManager = DateManager()
#State var viewModel = TrainingSessionViewModel()
#State var sessionDate = Date.now
#State var startTime = Date.now
#State var endTime = Date.now.addingTimeInterval(3600)
var completionHandler: ((Result<Action, Error>) -> Void)?
// MARK: - Local Views
var cancelButton: some View {
Button(action: { self.onCancel() }) {
Text("Cancel")
}
}
var saveButton: some View {
Button(action: { self.onSave() }) {
Text("Save")
}
}
var addInviteButton : some View {
Button(action: { sheetManager.showInvitesSheet.toggle() }) {
HStack {
Text("Add")
Image(systemName: "plus")
}
}
}
// MARK: - Main View
var body: some View {
NavigationView {
List {
Section(header: Text("Details")) {
TextField("Session Name", text: $viewModel.session.title)
TextField("Location", text: $viewModel.session.location)
}
Section {
DatePicker(selection: $sessionDate, displayedComponents: .date) {
Text("Date")
}
.onChange(of: sessionDate, perform: { _ in
viewModel.session.date = dateManager.dateToStr(date: sessionDate)
})
DatePicker(selection: $startTime, displayedComponents: .hourAndMinute) {
Text("Start Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: startTime, perform: { _ in
viewModel.session.startTime = dateManager.timeToStr(date: startTime)
})
DatePicker(selection: $endTime, displayedComponents: .hourAndMinute) {
Text("End Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: endTime, perform: { _ in
viewModel.session.endTime = dateManager.timeToStr(date: endTime)
})
}
Section {
HStack {
Text("Clients")
Spacer()
Button(action: { self.sheetManager.showInvitesSheet.toggle() }) {
Text("Edit").foregroundColor(.blue)
}
}
ForEach(viewModel.session.invites, id: \.self) { clientID in
self.createClientListElement(id: clientID)
}
.onDelete(perform: deleteInvite)
}
Section(header: Text("Notes")) {
TextField("Add notes here...", text: $viewModel.session.notes)
}
if mode == .edit {
Section {
HStack {
Spacer()
Button("Delete Session") {
sheetManager.showActionSheet.toggle()
}
.foregroundColor(.red)
Spacer()
}
}
}
}
.navigationTitle(mode == .new ? "New Training Session" : "Edit Training Session")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading: cancelButton,
trailing: saveButton)
.actionSheet(isPresented: $sheetManager.showActionSheet) {
ActionSheet(title: Text("Are you sure?"),
buttons: [
.destructive(Text("Delete Session"), action: { self.onDelete() }),
.cancel()
])
}
.sheet(isPresented: $sheetManager.showInvitesSheet) {
SessionInvitesView(sessionViewModel: $viewModel)
}
}
}
func createClientListElement(id: String) -> some View {
let client = clients.first(where: { $0.id == id })
if let client = client {
return AnyView(ClientListItem(client: client))
}
else {
return AnyView(Text("Invalid Client ID: \(id)"))
}
}
func deleteInvite(indexSet: IndexSet) {
viewModel.session.invites.remove(atOffsets: indexSet)
}
// MARK: - Local Event Handlers
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
func onSave() {
self.viewModel.onDone()
self.dismiss()
}
func onDelete() {
self.viewModel.onDelete()
self.dismiss()
self.completionHandler?(.success(.delete))
}
// MARK: - Sheet Management
class SheetManager : ObservableObject {
#Published var showActionSheet = false
#Published var showInvitesSheet = false
}
}
struct TrainingSessionEditView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionEditView(viewModel: TrainingSessionViewModel(session: buildSampleTrainingSession()))
}
}
I'm happy to include any of the other files if you think it would help. Thanks in advance!

How to update filtered list in swiftui, when the value in the filter is changed?

Usual caveat of being new to swiftui and apologies is this is a simple question.
I have a view where I have a date picker, as well as two arrows to increase/decrease the day. When this date is update, I am trying to filter a list of 'sessions' from the database which match the currently displayed date.
I have a filteredSessions variable which applies a filter to all 'sessions' from the database. However I do not seem to have that filter refreshed each time the date is changed.
I have the date to be used stored as a "#State" object in the view. I thought this would trigger the view to update whenever that field is changed? However I have run the debugger and found the 'filteredSessions' variable is only called once, and not when the date is changed (either by the picker or the buttons).
Is there something I'm missing here? Do I need a special way to 'bind' this date value to the list because it isn't directly used by the display?
Code below. Thanks
import SwiftUI
struct TrainingSessionListView: View {
#StateObject var viewModel = TrainingSessionsViewModel()
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(session: session)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 0, trailing: 0))
Spacer()
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
print("filteredSessions called")
return viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}
There are two approaches and for your case and for what you described I would take the first one. I only use the second approach if I have more complex filters and tasks
You can directly set the filter on the ForEach this will ensure it gets updated whenever the displayDate changes.
ForEach(viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }) { session in
sessionListItem(session: session)
}
Or you can like CouchDeveloper said, introduce a new state variable and to trigger a State change you would use the willSet extension (doesn't exist in binding but you can create it)
For this second option you could do something like this.
Start create the Binding extension for the didSet and willSet
extension Binding {
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
let snapshot = self.wrappedValue
self.wrappedValue = $0
execute(snapshot)
}
)
}
func willSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
Introduce the new state variable
#State var filteredSessions: [TrainingSession] = []
// removing the other var
We introduce the function that will update the State var
func filterSessions(_ filter: Date) {
filteredSessions = viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: date) }
}
We update the DatePicker to run the function using the willSet
DatePicker("", selection: $displayDate.willSet { self.filterSessions($0) }, displayedComponents: .date)
And lastly we add a onAppear so we fill the filteredSessions immidiatly (if you want)
.onAppear { filterSessions(displayDate) } // uses the displayDate that you set as initial value
Don't forget in your increaseDay() and decreaseDay() functions to add the following after the addTimeInterval
self.filterSessions(displayDate)
As I said, this second method might be better for more complex filters
Thank you all for your responses. I'm not sure what the issue was originally but it seems updating my view to use Firebase's #FirestoreQuery to access the collection updates the var filteredSessions... much better than what I had before.
New code below seems to be working nicely now.
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionListView: View {
#FirestoreQuery(collectionPath: "training_sessions") var sessions : [TrainingSession]
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var todayButton : some View {
Button(action: { self.displayDate = Date.now }) {
Text("Today")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(sessionId: session.id!)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 10, trailing: 0))
if filteredSessions.isEmpty {
Spacer()
Text("No Training Sessions found")
} else {
List {
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
}
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: todayButton, trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
return sessions.filter { $0.date == dateManager.dateToStr(date: displayDate)}
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}

Picker conflicts with NavigationLink gesture

I am trying to create the following card view.
With the following code to achieve it.
struct SimpleGame: Identifiable, Hashable {
var id = UUID()
let name: String
}
enum PlayingStatus: String {
case In = "I"
case Out = "O"
case Undecided = "U"
}
struct TestView: View {
let games: [SimpleGame] = [
.init(name: "First"),
.init(name: "Second")
]
#State private var currentStatus: PlayingStatus = .Undecided
var body: some View {
NavigationView {
List(games) { game in
Section {
VStack {
NavigationLink(value: game) {
Text("\(game.name)")
}
Divider()
Picker("Going?", selection: $currentStatus) {
Text("No Response")
.tag(PlayingStatus.Undecided)
Text("Going")
.tag(PlayingStatus.In)
Text("Not going")
.tag(PlayingStatus.Out)
}
.font(.body)
}
}
}
.navigationDestination(for: Game.self) { game in
Text("Detail View")
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Upcoming")
}
}
}
But a tap on element wrapped by NavigationLink is registering as a tap on the Picker. Anyone know of a way around this?
iOS 16/Xcode 14
you could try this:
List {
ForEach(games, id: \.name) { game in
Section {
NavigationLink(value: game) {
Text("\(game.name)")
}
// -- here
VStack {
Divider()
Picker("Going?", selection: $currentStatus) {
Text("No Response").tag(PlayingStatus.Undecided)
Text("Going").tag(PlayingStatus.In)
Text("Not going").tag(PlayingStatus.Out)
}
.font(.body)
}
}
}
}
What often works for me is extending the view.
struct TestView: View {
var body: some View {
List {
ForEach(games, id: \.name) { game in
Section {
NavigationLink(value: game) {
Text("\(game.name)")
}
// -- here
VStack {
Divider()
picker
}
}
}
}
}
}
Extension TestView {
private var picker: some View {
Picker("Going?", selection: $currentStatus) {
Text("No Response").tag(PlayingStatus.Undecided)
Text("Going").tag(PlayingStatus.In)
Text("Not going").tag(PlayingStatus.Out)
}
.font(.body)
}
}

SwiftUI: How to select multi items(image) with ForEach?

I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.

Update view bugs when data in firebase changes

This is when I launch the app and it looks okThis is when I change some data from firebase, as you can see some data is not arranged in the right way and some rectangles are doubleI'm currently working on this project where I need do read data from Firebase and update my views from the changes. Every time my data changes on firebase my views bugs in strange ways.
This is my Student class
struct Student : Identifiable {
var id : String
var name : String
var surname : String
var isAbsent : Bool
var isBathroom : Bool
}
Here where I get my data from firebase and publish to get all other views the data
class getStudents: ObservableObject {
#Published var datas = [Student]()
init() {
let db = Firestore.firestore()
let classRef = db.collection("5SA")
classRef.order(by: "surname").addSnapshotListener {(snap,error) in
if error != nil{
print((error?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("name")as! String
let surname = i.document.get("surname") as! String
let isAbsent = i.document.get("absent") as! Bool
let isBathroom = i.document.get("bathroom") as! Bool
self.datas.append(Student(id: id, name: name, surname: surname, isAbsent: isAbsent, isBathroom: isBathroom))
if i.type == .modified{
let isAbsent = i.document.get("absent") as! Bool
let isBathroom = i.document.get("bathroom") as! Bool
for j in 0..<self.datas.count{
if self.datas[j].id == id{
self.datas[j].isAbsent = isAbsent
self.datas[j].isBathroom = isBathroom
}
}
}
}
}
}
}
This are my views. Apart from the landscape on the iPad simulator the bugs every time.
What I'm trying to achieve is creating like a class app. Where there are students absent, in the toilet or present. I want to to show every student with a rectangle with his name on it and to change color of the rectangles every time the bools on my fire base changes(isAbsent,isBathroom). But when I try to change value on firebase the rectangles multiplies and do strange thinks.
struct ContentView: View {
#ObservedObject var students = getStudents()
var body: some View {
NavigationView{
VStack{
ScrollView{
Text("Menu")
Menu().background(Color.white)
Text("Future verifiche")
HStack{
ScrollView(.horizontal, showsIndicators: false){
RoundedRectangle(cornerRadius: 20).frame(width:300,height: 300).foregroundColor(.white).overlay(Text("V. Matematica"))
}
}
Text("Oggi")
HStack{
Spacer()
Text("Assenti ")
Spacer()
Text("In bagno")
Spacer()
Text("Presenti")
Spacer()
}
HStack{
Spacer()
ScrollView{
ForEach(students.datas,id: \.id){student in
VStack(spacing: 15){
if student.isAbsent{
RoundedRectangle(cornerRadius: 20).frame(width:200,height: 100).foregroundColor(self.getColor(isAbsent: student.isAbsent, isBathroom: student.isBathroom)).overlay(Text(student.name))
}
}
}
}
Spacer()
ScrollView{
ForEach(students.datas,id: \.id){student in
VStack(spacing: 15){
if student.isBathroom{
RoundedRectangle(cornerRadius: 20).frame(width:200,height: 100).foregroundColor(self.getColor(isAbsent: student.isAbsent, isBathroom: student.isBathroom)).overlay(Text(student.name))
}
}
}
}
Spacer()
ScrollView{
ForEach(students.datas,id: \.id){student in
VStack(spacing: 15){
if !student.isAbsent && !student.isBathroom{
RoundedRectangle(cornerRadius: 20).frame(width:200,height: 100).foregroundColor(self.getColor(isAbsent: student.isAbsent, isBathroom: student.isBathroom)).overlay(Text(student.name))
}
}
}
}
Spacer()
}
}
}.navigationBarTitle("Classe 5SA",displayMode: .inline)
}.navigationViewStyle(StackNavigationViewStyle())
}
func getColor(isAbsent: Bool, isBathroom : Bool) -> Color {
if isAbsent{
return Color.red
}
if isBathroom{
return Color.blue
}
return Color.green
}
}
struct Menu : View {
var body : some View{
HStack{
NavigationLink(destination: Studenti()){
RoundedRectangle(cornerRadius: 30).frame(width: 150,height: 150).foregroundColor(.red).opacity(0.5).overlay(Text("Studenti").foregroundColor(.black))
}
NavigationLink(destination: Text("Overview")){
RoundedRectangle(cornerRadius: 30).frame(width: 150,height: 150).foregroundColor(.green).opacity(0.5).overlay(Text("Overview").foregroundColor(.black))
}
NavigationLink(destination: Text("Orario")){
RoundedRectangle(cornerRadius: 30).frame(width: 150,height: 150).foregroundColor(.blue).opacity(0.5).overlay(Text("Orario").foregroundColor(.black))
}
NavigationLink(destination: Text("Calendario")){
RoundedRectangle(cornerRadius: 30).frame(width: 150,height: 150).foregroundColor(.orange).opacity(0.5).overlay(Text("Calendario").foregroundColor(.black))
}
}
}
}
struct Studenti : View {
#ObservedObject var students = getStudents()
var body : some View {
ForEach(students.datas,id: \.id){student in
VStack{
Rectangle().frame(width:300,height: 150).foregroundColor(self.getColor(isAbsent: student.isAbsent, isBathroom: student.isBathroom)).overlay(Text(student.name))
}
}
}
func getColor(isAbsent: Bool, isBathroom : Bool) -> Color {
if isAbsent{
return Color.red
}
if isBathroom{
return Color.blue
}
return Color.green
}
}

Resources