How do I update the data in the tabview after the one-to-many coredata has been modified? - swift

Purpose
I want to update the data in the tabview automatically when I return to the RootView after I rename the tag name in the tagManagemenView.
Current Status
When I do delete operation in the TagManagementView, the RootView is able to update automatically.
However, If the Tag name is modified, the RootView display will not be updated, but clicking into the ItemDetailsView is able to display the latest modified name. Only the display of the RootView is not updated.
Background and code
Item and Tag are created using coredata and have a one-to-many relationship, where one Item corresponds to multiple Tags
// RootView
struct RootView: View {
#State private var selection = 0
var body: some View {
NavigationView {
TabView(selection: $selection) {
ItemListView()
.tabItem {
Label ("Items",systemImage: "shippingbox")
}
.tag(0)
Settings()
.tabItem{
Label("Settings", systemImage: "gearshape")
}
.tag(1)
}
.navigationTitle(selection == 0 ? "Items" : "Settings")
}
.navigationViewStyle(.stack)
}
}
// ItemListView
struct ItemListView: View {
#FetchRequest var items: FetchedResults<Item>
#State private var itemDetailViewIsShow: Bool = false
#State private var selectedItem: Item? = nil
init() {
var predicate = NSPredicate(format: "TRUEPREDICATE"))
_items = FetchRequest(fetchRequest: Item.fetchRequest(predicate))
}
var body: some View {
ForEach(items) { item in
Button(action: {
self.selectedItem = item
self.itemDetailViewIsShow = true
}, label: {
ItemCellView(item: item)
})
}
if selectedItem != nil {
NavigationLink (
destination: ItemDetailView(item: selectedItem!, detailViewIsShow: $itemDetailViewIsShow),
isActive: $itemDetailViewIsShow
) {
EmptyView()
}
.isDetailLink(false)
}
}
}
// TagManagementView
struct TagManagementView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Tag.entity(), sortDescriptors: []) var allTags: FetchedResults<Tag>
#State var isShowDeleteAlert = false
#State var showModel = false
#State var selected: Tag?
var body: some View {
ZStack {
List {
ForEach(allTags) { tag in
TagCellView(tag: tag)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive, action: {
isShowDeleteAlert = true
selected = tag
}, label: {
Label("Delete", systemImage: "trash")
.foregroundColor(.white)
})
Button(action: {
showModel = true
selected = tag
}, label: {
Label("Edit", systemImage: "square.and.pencil")
.foregroundColor(.white)
})
}
}
.confirmationDialog("Delete confirm", isPresented: self.$isShowDeleteAlert, titleVisibility: .visible) {
Button("Delete", role: .destructive) {
if self.selected != nil {
self.selected!.delete(context: context)
}
}
Button(role: .cancel, action: {
isShowDeleteAlert = false
}, label: {
Text("Cancel")
.font(.system(size: 17, weight: .medium))
})
}
}
if self.showModel {
// background...
Color("mask").edgesIgnoringSafeArea(.all)
TagEditorModal(selected: self.$selected, isShowing: self.$showModel)
}
}
}
}
// TagEditorModal
struct TagEditorModal: View {
#Environment(\.managedObjectContext) var context
#State var tagName: String = ""
#Binding var isShowing: Bool
#Binding var selector: Tag?
init (selected: Binding<Tag?>, isShowing: Binding<Bool>) {
_isShowing = isShowing
_selector = selected
_tagName = .init(wrappedValue: selected.wrappedValue!.name)
}
var body: some View {
VStack{
TextField("Tag name", text: self.$tagName)
HStack {
Button(action: {
self.isShowing = false
}) {
Text("Cancel")
}
Button(action: {
self.selector!.update(name: self.tagName, context: context)
self.isShowing = false
}, label: {
Text("Submit")
})
}
}
}
}
// update tagName func
extension Tag {
func update(name: String, context: NSManagedObjectContext) {
self.name = name
self.updatedAt = Date()
self.objectWillChange.send()
try? context.save()
}
}
// ItemCellView
struct ItemCellView: View {
#Environment(\.managedObjectContext) var context
#ObservedObject var item: Item
var body: some View {
VStack {
Text(item.name)
TagListView(tags: .constant(item.tags))
}
}
}
// tagListView
struct TagListView: View {
#Binding var tags: [Tag]
#State private var totalHeight = CGFloat.zero
var body: some View {
VStack {
GeometryReader { geo in
VStack(alignment: .leading,spacing: 10) {
ForEach (getRows(screenWidth: geo.size.width), id: \.self) {rows in
HStack(spacing: 4) {
ForEach (rows) { tag in
Text(tag.name)
.font(.system(size: 10))
.fontWeight(.medium)
.lineLimit(1)
.cornerRadius(40)
}
}
}
}
.frame(width: geo.size.width, alignment: .leading)
.background(viewHeightReader($totalHeight))
}
}
.frame(height: totalHeight)
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geo -> Color in
let rect = geo.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
func getRows(screenWidth: CGFloat) -> [[Tag]] {
var rows: [[Tag]] = []
var currentRow: [Tag] = []
var totalWidth: CGFloat = 0
self.tags.forEach{ tag in
totalWidth += (tag.size + 24)
if totalWidth > (screenWidth) {
totalWidth = (!currentRow.isEmpty || rows.isEmpty ? (tag.size + 24) : 0)
rows.append(currentRow)
currentRow.removeAll()
currentRow.append(tag)
} else {
currentRow.append(tag)
}
}
if !currentRow.isEmpty {
rows.append(currentRow)
currentRow.removeAll()
}
return rows
}
}

I added a TagCellView and then used an #ObservedObject for the tag
struct TagChipsView: View {
#ObservedObject var tag: Tag
let verticalPadding: CGFloat = 2.0
let horizontalPadding: CGFloat = 8.0
var body: some View {
Text(tag.name)
}
}

Related

How to pass different color when appending to an Array in SwiftUI?

In my ContentView I have two buttons, red and green, when I click on each of them i change and then append them to an empty array that gets filled and display its items in my SecondView.
I would like to show a green name when i tap on the green button and a red one when i tap on the red button, how can i achieve this?
Here is the code:
ContentView
struct ContentView: View {
#State private var countdown = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect().eraseToAnyPublisher()
#State private var timeRemaining = 10
#State private var isActive = false
#State private var names = ["Steve", "Bill", "Jeff", "Elon", "Michael", "John", "Steven", "Paul"]
#State private var appearedNames = [String]()
#State private var currentPerson = ""
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: SecondView(appearedNames: $appearedNames, countdown: countdown), isActive: $isActive) {
EmptyView()
}
VStack {
Text("\(timeRemaining)")
.font(.largeTitle)
.padding()
.onReceive(countdown) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
if timeRemaining == 0 {
isActive = true
}
}
HStack {
Button {
appearedNames.append(currentPerson)
changePerson()
} label: {
Text(currentPerson)
.foregroundColor(.red)
.font(.largeTitle)
}
Button {
appearedNames.append(currentPerson)
changePerson()
} label: {
Text(currentPerson)
.foregroundColor(.green)
.font(.largeTitle)
}
}
}
.onAppear {
changePerson()
}
}
}
}
func changePerson() {
if names.count > 0 {
let index = Int.random(in: 0..<names.count)
currentPerson = names[index]
names.remove(at: index)
}
}
}
SecondView
import SwiftUI
import Combine
struct SecondView: View {
#Binding var appearedNames: [String]
var countdown: AnyPublisher<Date,Never>
#State private var counter = 0
var body: some View {
VStack {
ScrollViewReader { proxy in
ScrollView(showsIndicators: false) {
ForEach(0...counter, id: \.self) { index in
Text(appearedNames[index])
.font(.system(size: 70))
.id(index)
.onAppear {
proxy.scrollTo(index)
}
}
}
}
}
.navigationBarBackButtonHidden(true)
.onReceive(countdown) { _ in
if counter < appearedNames.count - 1 {
counter += 1
}
}
}
}
Instead of [String] store your choice in a struct with the chosen color:
// use this struct to save the chosen color with the name
struct ColoredName: Identifiable{
let id = UUID()
var name: String
var color: Color
}
struct ContentView: View {
#State private var countdown = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect().eraseToAnyPublisher()
#State private var timeRemaining = 10
#State private var isActive = false
#State private var names = ["Steve", "Bill", "Jeff", "Elon", "Michael", "John", "Steven", "Paul"]
//change this to the appropriate type
#State private var appearedNames = [ColoredName]()
#State private var currentPerson = ""
var body: some View {
NavigationView {
ZStack {
//pass the array on to the second view
NavigationLink(destination: SecondView(appearedNames: $appearedNames, countdown: countdown), isActive: $isActive) {
EmptyView()
}
VStack {
Text("\(timeRemaining)")
.font(.largeTitle)
.padding()
.onReceive(countdown) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
if timeRemaining == 0 {
isActive = true
}
}
HStack {
Button {
//Create and append your struct to the array
appearedNames.append(ColoredName(name: currentPerson, color: .red))
changePerson()
} label: {
Text(currentPerson)
.foregroundColor(.red)
.font(.largeTitle)
}
Button {
//Create and append your struct to the array
appearedNames.append(ColoredName(name: currentPerson, color: .green))
changePerson()
} label: {
Text(currentPerson)
.foregroundColor(.green)
.font(.largeTitle)
}
}
}
.onAppear {
changePerson()
}
}
}
}
func changePerson() {
if names.count > 0 {
let index = Int.random(in: 0..<names.count)
currentPerson = names[index]
names.remove(at: index)
}
}
}
struct SecondView: View {
#Binding var appearedNames: [ColoredName]
var countdown: AnyPublisher<Date,Never>
#State private var counter = 0
var body: some View {
VStack {
ScrollViewReader { proxy in
ScrollView(showsIndicators: false) {
ForEach(0...counter, id: \.self) { index in
//Access the properties of the colored name by index
Text(appearedNames[index].name)
.font(.system(size: 70))
.foregroundColor(appearedNames[index].color)
.id(index)
.onAppear {
proxy.scrollTo(index)
}
}
}
}
}
.navigationBarBackButtonHidden(true)
.onReceive(countdown) { _ in
if counter < appearedNames.count - 1 {
counter += 1
}
}
}
}
But I do not think iterating over an index is a good solution in any case. To solve your problem with the scrolling you can do:
struct SecondView: View {
// This does not have to be a #Binding as long as you do not
// manipulate the array from this view
// this holds the source for the names to present
#Binding var appearedNames: [ColoredName]
var countdown: AnyPublisher<Date,Never>
//Add this to show the names
#State private var presentedPersons : [ColoredName] = []
#State private var counter = 0
var body: some View {
VStack {
ScrollViewReader { proxy in
ScrollView(showsIndicators: false) {
// iterate over the array
ForEach(presentedPersons) { person in
//Access the properties of the struct to populate and format
// the text
Text(person.name)
// No need to set the `id` manually as our struct allready
// conforms to Identifiable
.font(.system(size: 70))
.foregroundColor(person.color)
.onAppear {
// As the struct is Identifiable the id of the View is the
// id of the struct and we can scroll by
proxy.scrollTo(person.id)
}
}
}
}
}
.navigationBarBackButtonHidden(true)
.onReceive(countdown) { _ in
if counter < appearedNames.count{
presentedPersons.append(appearedNames[counter])
counter += 1
}
}
}
}

SwiftUI Animation on property change?

I want to animate the appearance of an item in a list. The list looks like this:
Text("Jim")
Text("Jonas")
TextField("New Player")
TextField("New Player") //should be animated when appearing (not shown until a name is typed in the first "New Player")
The last TextField should be hidden until newPersonName.count > 0 and then appear with an animation.
This is the code:
struct V_NewSession: View {
#State var newPersonName: String = ""
#State var participants: [String] = [
"Jim",
"Jonas"
]
var body: some View {
VStack(alignment: .leading) {
ForEach(0...self.participants.count + 1, id: \.self) { i in
// Without this if statement, the animation works
// but the Textfield shouldn't be shown until a name is typed
if (!(self.newPersonName.count == 0 && i == self.participants.count + 1)) {
HStack {
if(i < self.participants.count) {
Text(self.participants[i])
} else {
TextField("New Player",
text: $newPersonName,
onEditingChanged: { focused in
if (self.newPersonName.count == 0) {
if (!focused) {
handleNewPlayerEntry()
}
}
})
}
}
}
}
.transition(.scale)
Spacer()
}
}
func handleNewPlayerEntry() {
if(newPersonName.count > 0) {
withAnimation(.spring()) {
participants.append(newPersonName)
newPersonName = ""
}
}
}
}
I know withAnimation(...) only applies to participants.append(newPersonName), but how can I get the animation to work on the property change in the if-statement?
if ((!(self.newPersonName.count == 0 && i == self.participants.count + 1)).animation()) doesn't work.
Your example code won't compile for me, but here's a trivial example of using Combine inside a ViewModel to control whether a second TextField appears based on a condition:
import SwiftUI
import Combine
class ViewModel : ObservableObject {
#Published var textFieldValue1 = ""
#Published var textFieldValue2 = "Second field"
#Published var showTextField2 = false
private var cancellable : AnyCancellable?
init() {
cancellable = $textFieldValue1.sink { value in
withAnimation {
self.showTextField2 = !value.isEmpty
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
TextField("", text: $viewModel.textFieldValue1)
.textFieldStyle(RoundedBorderTextFieldStyle())
if viewModel.showTextField2 {
TextField("", text: $viewModel.textFieldValue2)
.textFieldStyle(RoundedBorderTextFieldStyle())
.transition(.scale)
}
}
}
}
Is that approaching what you're attempting to build ?
struct ContentView: View {
#State var newPersonName: String = ""
#State var participants: [String] = [
"Jim",
"Jonas"
]
#State var editingNewPlayer = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
ForEach(participants, id: \.self) { participant in
Text(participant)
.padding(.trailing)
Divider()
}
Button(action: handleNewPlayerEntry, label: {
TextField("New Player", text: $newPersonName, onEditingChanged: { edit in
editingNewPlayer = edit
}, onCommit: handleNewPlayerEntry)
})
if editingNewPlayer {
Button(action: handleNewPlayerEntry, label: {
TextField("New Player", text: $newPersonName) { edit in
editingNewPlayer = false
}
})
}
}
.padding(.leading)
.frame(maxHeight: .infinity, alignment: .top)
.transition(.opacity)
.animation(.easeIn)
}
func handleNewPlayerEntry() {
if newPersonName.count > 0 {
withAnimation(.spring()) {
participants.append(newPersonName)
newPersonName = ""
editingNewPlayer = false
}
}
}
}

Why do I lose Data here?(SwiftUI)

I have two pages in my app TodayPage and CalendarList page.
I use EnvironmentObject wrapper to pass data between these two pages.
When TodayPage appears on onAppear modifier I call a function to generate days of calendar for me till now everything works fine when I add text to the list of TodayPage then go to the calendarList page and come back again to TodayPage all of the text that I addd to list are gone.I find out I can avoid lost of data by adding simple if to onAppear but I'm not sure this solution is right.
I have to upload lots of code ,Thanks for your help
( DataModel ) :
import SwiftUI
import Foundation
import Combine
struct Day : Identifiable {
var id = UUID()
var name : String
var date : String
var month : String
var List : [Text1?]
}
struct Text1 : Identifiable , Hashable{
var id = UUID()
var name: String
var color: Color
}
class AppState : ObservableObject {
#Published var dataLoaded = false
#Published var allDays : [Day] = [.init(name : "",date: "",month: "",List : [])]
func getDays(number: Int) -> [Day] {
let today = Date()
let formatter = DateFormatter()
return (0..<number).map { index -> Day in
let date = Calendar.current.date(byAdding: .day, value: index, to: today) ?? Date()
return Day(name: date.dayOfWeek(withFormatter: formatter) ?? "", date: "\(Calendar.current.component(.day, from: date))", month: date.nameOfMonth(withFormatter: formatter) ?? "", List: [])
}
}
}
extension Date {
func dayOfWeek(withFormatter dateFormatter: DateFormatter) -> String? {
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: self).capitalized
}
func nameOfMonth(withFormatter dateFormatter: DateFormatter) -> String? {
dateFormatter.dateFormat = "LLLL"
return dateFormatter.string(from: self).capitalized
}
}
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(name: "", color: .clear)] //start with one empty item
func saveToAppState(appState: AppState) {
appState.allDays[0].List.append(contentsOf: textItemsToAdd.filter {
!$0.name.isEmpty })
}
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.name ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, name: newValue, color: .clear)
}
}
}
}
List view :
struct ListView: View {
#State private var showAddListView = false
#EnvironmentObject var appState : AppState
#Binding var dayList : [Text1?]
var title : String
var body: some View {
NavigationView {
VStack {
ZStack {
List(dayList, id : \.self){ text in
Text(text?.name ?? "")
}
if showAddListView {
AddListView(showAddListView: $showAddListView)
.offset(y:-100)
}
}
}
.navigationTitle(title)
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
pop up menu View(for adding text into the list)
struct AddListView: View {
#Binding var showAddListView : Bool
#EnvironmentObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(name: "", color: .clear)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
TodayPage View :
struct TodayPage: View {
#EnvironmentObject var appState : AppState
var body: some View {
ListView(dayList: $appState.allDays[0].List, title: "Today")
.onAppear {
// To avoid data lost , we can use simple if below but I'm not sure it's a right solution
// if appState.dataLoaded == false {
appState.allDays = appState.getDays(number: 365)
// appState.dataLoaded = true
// }
}
}
}
CalendarListPage :
struct CalendarList: View {
#EnvironmentObject var appState : AppState
var body: some View {
NavigationView {
List {
ForEach(appState.allDays.indices, id:\.self) { index in
NavigationLink(destination: ListView(appState: _appState, dayList: $appState.allDays[index].List, title: appState.allDays[index].name).navigationBarTitleDisplayMode(.inline) ) {
HStack(alignment: .top) {
RoundedRectangle(cornerRadius: 23)
.frame(width: 74, height: 74)
.foregroundColor(Color.blue)
.overlay(
VStack {
Text(appState.allDays[index].date)
.font(.system(size: 35, weight: .regular))
.foregroundColor(.white)
Text(appState.allDays[index].month)
.foregroundColor(.white)
}
)
.padding(.trailing ,4)
VStack(alignment: .leading, spacing: 5) {
Text(appState.allDays[index].name)
.font(.system(size: 20, weight: .semibold))
}
}
.padding(.vertical ,6)
}
}
}
.navigationTitle("Calendar")
}.onAppear {
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
and finally TabBar :
struct TabBar: View {
var body: some View {
let appState = AppState()
TabView {
TodayPage().tabItem {
Image(systemName: "info.circle")
Text("Today")
}
CalendarList().tabItem {
Image(systemName: "square.fill.text.grid.1x2")
Text("Calendar")
}
}
.environmentObject(appState)
}
}
Right now, because your let appState is inside the body of TabBar, it gets recreated every time TabBar is rendered. Instead, store it as a #StateObject (or #ObservedObject if you are pre iOS 14):
struct TabBar: View {
#StateObject var appState = AppState()
var body: some View {
TabView {
TodayPage().tabItem {
Image(systemName: "info.circle")
Text("Today")
}
CalendarList().tabItem {
Image(systemName: "square.fill.text.grid.1x2")
Text("Calendar")
}
}
.onAppear {
appState.allDays = appState.getDays(number: 365)
}
.environmentObject(appState)
}
}
Then, remove your other onAppear on TodayPage

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

CircleImage keeps changing colour after selecting an option in picker and adding to list

I'm having this weird issue where the colour for an item in a list changes when a new item with a different colour is added, essentially it doesn't retain its colour-value but takes up a new one.
What I'm trying to do is to show a colour that corresponds to the priority level the user has selected.
Here is the code:
struct PriorityGreen: View {
var body: some View {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(Color.green)
}
}
struct PriorityYellow: View {
var body: some View {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(Color.yellow)
}
}
struct PriorityOrange: View {
var body: some View {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(Color.orange)
}
}
struct PriorityRed: View {
var body: some View {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(Color.red)
}
}
Code for view
import SwiftUI
struct AppView: View {
#ObservedObject var data = Model()
#State var showViewTwo = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(data.arrayOfTask, id: \.self) { row in
HStack {
if self.data.priority == 0 {
PriorityGreen()
} else if self.data.priority == 1 {
PriorityYellow()
} else if self.data.priority == 2 {
PriorityOrange()
} else if self.data.priority == 3 {
PriorityRed()
}
Text("\(row)")
}
}
.onDelete(perform: removeItems).animation(.default)
}
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
}
.navigationBarTitle("Tasks")
.navigationBarItems(leading:
EditButton().animation(.default),
trailing: Button(action: {
self.showViewTwo.toggle()
}) {
Text("New task")
}.sheet(isPresented: $showViewTwo) {
ViewTwo(data: self.data, showViewTwo: self.$showViewTwo)
})
}
}
func removeItems(at offset: IndexSet) {
data.arrayOfTask.remove(atOffsets: offset)
}
}
struct AppView_Previews: PreviewProvider {
static var previews: some View {
AppView()
}
}
struct ViewTwo: View {
#State var data: Model
#State var newName = ""
#State var newCatergory = ""
#State var newPriorityLevel = ""
#State var defaultPriorityLevel = 1
#State var priorityTypes = ["low", "medium", "high", "critical"]
#Binding var showViewTwo: Bool
var body: some View {
NavigationView {
Form {
Section(header: Text("Add task name")) {
TextField("Name", text: $newName)
/*
This section will be implementated later on
TextField("Catergory", text: $newCatergory)
*/
}
Section(header: Text("Select task priority")) {
Picker("Priority Levels", selection: $defaultPriorityLevel) {
ForEach(0..<priorityTypes.count) {
Text(self.priorityTypes[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
.navigationBarTitle("New task details")
.navigationBarItems(trailing:
Button("Save") {
self.showViewTwo.toggle()
self.data.taskName = self.newName
self.data.arrayOfTask.append(self.newName)
self.data.priority = self.defaultPriorityLevel
})
}
}
}
struct PriorityCirleView: View {
var body: some View {
Circle()
.frame(width: 20, height: 20)
.foregroundColor(Color.green)
}
}
import SwiftUI
enum Catergory {
case work
case home
case family
case health
case bills
}
enum Priority {
case low
case medium
case high
case critical
}
class Model: ObservableObject {
#Published var taskName = ""
#Published var taskCategory = ""
#Published var priority = 0
#Published var arrayOfTask = [String]()
}
This gif demonstrates the problem more clearly
(Gif)[https://imgur.com/a/ffzpSft]
You only have one priority in your model instead of a priority per task.
Change your model to this:
class Model: ObservableObject {
struct Task {
var taskName = ""
var taskCategory = ""
var priority = 0
}
#Published var arrayOfTask = [Task]()
}
And update your code to use the new model:
struct AppView: View {
#ObservedObject var data = Model()
#State var showViewTwo = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(data.arrayOfTask, id: \.taskName) { task in
HStack {
if task.priority == 0 {
PriorityGreen()
} else if task.priority == 1 {
PriorityYellow()
} else if task.priority == 2 {
PriorityOrange()
} else if task.priority == 3 {
PriorityRed()
}
Text("\(task.taskName)")
}
}
.onDelete(perform: removeItems).animation(.default)
}
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
}
.navigationBarTitle("Tasks")
.navigationBarItems(leading:
EditButton().animation(.default),
trailing: Button(action: {
self.showViewTwo.toggle()
}) {
Text("New task")
}.sheet(isPresented: $showViewTwo) {
ViewTwo(data: self.data, showViewTwo: self.$showViewTwo)
})
}
}
func removeItems(at offset: IndexSet) {
data.arrayOfTask.remove(atOffsets: offset)
}
}
struct ViewTwo: View {
#State var data: Model
#State var newName = ""
#State var newCatergory = ""
#State var newPriorityLevel = ""
#State var defaultPriorityLevel = 1
#State var priorityTypes = ["low", "medium", "high", "critical"]
#Binding var showViewTwo: Bool
var body: some View {
NavigationView {
Form {
Section(header: Text("Add task name")) {
TextField("Name", text: $newName)
/*
This section will be implementated later on
TextField("Catergory", text: $newCatergory)
*/
}
Section(header: Text("Select task priority")) {
Picker("Priority Levels", selection: $defaultPriorityLevel) {
ForEach(0..<priorityTypes.count) {
Text(self.priorityTypes[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
.navigationBarTitle("New task details")
.navigationBarItems(trailing:
Button("Save") {
var task = Model.Task()
self.showViewTwo.toggle()
task.taskName = self.newName
task.priority = self.defaultPriorityLevel
self.data.arrayOfTask.append(task)
})
}
}
}
Using the taskName as the id is not a good idea. Update your Task struct to include a unique value:
class Model: ObservableObject {
struct Task: Identifiable {
static var uniqueID = 0
var taskName = ""
var taskCategory = ""
var priority = 0
var id = 0
init() {
Task.uniqueID += 1
self.id = Task.uniqueID
}
}
#Published var arrayOfTask = [Task]()
}
And then change:
ForEach(data.arrayOfTask, id: \.taskName) { task in
to
ForEach(data.arrayOfTask) { task in