I'm trying to append an array on a tap gesture, but it seems that it replaces any item in the array with the current one.
The Array and functions:
class ChipsViewModel: ObservableObject {
#Published var selectedCategories = [String]()
func appendArray(title:String) {
selectedCategories.append(title)
}
func removeFromArray(title:String){
if let index = selectedCategories.firstIndex(of: title) {
selectedCategories.remove(at: index)
}
}
}
The button itself:
#ObservedObject var viewModel = ChipsViewModel()
let systemImage: String
let titleKey: String
#State var isSelected: Bool
var body: some View {
HStack {
...
}.onTapGesture {
isSelected.toggle()
if(isSelected){
viewModel.appendArray(title: titleKey)
} else if (!isSelected){
viewModel.removeFromArray(title: titleKey)
}
print(viewModel.selectedCategories)
}
}
Not really sure why is this happening, it worked when i implemented a binding foreach, but using that setup the chips don't work as needed.
It looks like you are trying to keep state in sync in two places at the same time, it is usually not a good idea. It is best to keep it your view model
class ChipsViewModel: ObservableObject {
#Published var selectedCategories = [String]()
func isSelected(_ title: String) -> Bool {
selectedCategories.contains(title)
}
func toggleSelection(_ title: String) {
if let index = selectedCategories.firstIndex(of: title) {
selectedCategories.remove(at: index)
} else {
selectedCategories.append(title)
}
}
}
and then your View gets simpler and the state (i.e. whether a title is selected or not) cannot get out of sync:
#StateObject var viewModel = ChipsViewModel()
let systemImage: String
let titleKey: String
var isSelected: Bool {
viewModel.isSelected(titleKey)
}
var body: some View {
HStack {
//...
}
.onTapGesture {
viewModel.toggleSelection(titleKey)
}
}
The State needs to be initialized, like
let systemImage: String
let titleKey: String
#State var isSelected: Bool = false // << here !!
Related
I am trying to open a sheet when tapping on an item. I followed this questions Sheet inside ForEach doesn't loop over items SwiftUI answer. I get this Error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions, I don't understand what is causing it. I tried multiple solutions and they all lead to the same Error.
#State var selectedSong: Int? = nil
#State var songList: [AlbumSong] = []
VStack {
ForEach(songList.enumerated().reversed(), id: \.offset) { index, song in
HStack {
Text("\(index + 1).").padding(.leading, 8)
VStack {
Text(song.title)
Text(song.artist)
}
}.onTapGesture {
self.selectedSong = index
}
}
}
}
.sheet(item: self.$selectedSong) { selectedMovie in
SongPickerEdit(songList: $songList, songIndex: selectedMovie)
I also tried setting songIndex to being an AlbumSong and then implemented this sheet:
.sheet(item: self.$selectedSong) {
SongPickerEdit(songList: $songList, songIndex: self.songList[$0])
}
struct SongPickerEdit: View {
#Binding var songList: [AlbumSong]
#State var songIndex: Int?
var body: some View {
}
}
struct AlbumSong: Identifiable, Codable {
#DocumentID var id: String?
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
How about making selectedSong an AlbumSong?? The item: parameter needs to be an Identifiable binding, but Int is not Identifiable.
#State var selectedSong: AlbumSong? = nil
#State var songList: [AlbumSong] = []
var body: some View {
List {
ForEach(songList.enumerated().reversed(), id: \.offset) { index, song in
HStack {
Text("\(index + 1).").padding(.leading, 8)
VStack {
Text(song.title)
Text(song.artist)
}
}.onTapGesture {
self.selectedSong = song
}
}
}.sheet(item: $selectedSong) { song in
SongPickerEdit(songList: $songList, song: song)
}
}
Note that SongPickerEdit would look like this:
struct SongPickerEdit: View {
#State var song: AlbumSong
var body: some View {
Text("\(song.title), \(song.artist)")
}
}
If you really need the index for some reason, you can add the song list binding back in and use songList.index { $0.id == song.id } to find the index if the list is not too long.
Otherwise, you can make your own Identifiable type SongAndIndex that uses the same id as AlbumSong, but with an extra index property, and use that as the type of selectedSong.
A third way would be to use the sheet(isPresented:) overload, but this way you end up with 2 sources of truth:
#State var selectedSongIndex: Int? = nil {
didSet {
if selectedSongIndex != nil {
isSheetPresented = true
}
}
}
#State var isSheetPresented: Bool = false {
didSet {
if !isSheetPresented {
selectedSongIndex = nil
}
}
}
...
}.sheet(isPresented: $isSheetPresented) {
SongPickerEdit(songList: $songList, songIndex: selectedSongIndex)
}
selectedSongIndex also won't be set to nil when the user dismisses the sheet.
I have a simple SwiftUI view with a Picker containing a list of objects from a data array. The Picker lists the objects just fine, but the selected value is not being saved to the binding variable $selectedCar. It returns empty string. This is the view in question:
struct GarageSpace: View {
var currentUserID: String
#Environment(\.presentationMode) var presentationMode
#Binding var selectedPlaceID: String
#Binding var selectedPlaceName: String
#Binding var selectedPlaceDate: Date
#Binding var selectedSpaceID: String
#State var selectedCar: String
#Binding var cars: CarArrayObject
var body: some View {
VStack{
Group{
Picker("Car", selection: $selectedCar) {
if let cars = cars{
ForEach(cars.dataArray, id: \.self) {car in
let year = car.year! as String
let make = car.make as String
let model = car.model! as String
let string = year + " " + make + " " + model
Text(string) //displays correctly in Picker
}
}
}
Spacer()
if let cars = cars {
Button {
print("yes")
print(selectedCar) //returns empty string
} label: {
Text("Confirm")
}
}
}
}
}
}
The above view is displayed via a NavigationLink on the previous screen:
NavigationLink(destination: GarageSpace(currentUserID: currentUserID, selectedPlaceID: $selectedPlaceID, selectedPlaceName: $selectedPlaceName, selectedPlaceDate: $selectedPlaceDate, selectedSpaceID: $selectedSpaceID, selectedCar: "", cars: $cars)) {
}
This NavigationLink might be the culprit because I'm sending an empty string for selectedCar. However, it forces me to initialize a value with the NavigationLink.
Any ideas? Thanks!
EDIT:
Added a tag of type String, still same outcome:
Text(string).tag(car.carID)
EDIT: FOUND THE ISSUE! However, I'm still stumped. The selection variable is empty because I wasn't pressing on the Picker since I only had one item in the array. How can I get the Picker to "select" an item if it's the only one in the array by default?
With tag, all works well in my simple tests. Here is my test code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
GarageSpace()
}
}
struct GarageSpace: View {
#State var selectedCar: String = ""
#State var cars: CarArrayObject? = CarArrayObject(car: CarModel(make: "Ford"))
var body: some View {
VStack {
Group {
Picker("Car", selection: $selectedCar) {
if let cars = cars {
ForEach(cars.dataArray, id: \.self) { car in
Text(car.make).tag(car.carID)
}
}
}
Spacer()
if let cars = cars {
Button {
print("----> selectedCar carID: \(selectedCar)")
} label: {
Text("Show selected carID")
}
}
}
}
// optional, to select the first car
.onAppear {
if let cars = cars {
selectedCar = (cars.dataArray.first != nil) ? cars.dataArray.first!.carID : ""
}
}
}
}
struct CarModel: Hashable {
var make = ""
var carID = UUID().uuidString
}
class CarArrayObject: ObservableObject{
// for testing
#Published var dataArray = [CarModel(make: "Toyota"), CarModel(make: "Suzuki"), CarModel(make: "VW")]
/// USED FOR SINGLE CAR SELECTION
init(car: CarModel) {
self.dataArray.append(car)
}
/// USED FOR GETTING CARS FOR USER PROFILE
init(userID: String) {
// print("GET CARS FOR USER ID \(userID)")
// DataService.instance.downloadCarForUser(userID: userID) { (returnedCars) in
//
// let sortedCars = returnedCars.sorted { (car1, car2) -> Bool in
// return car1.dateCreated > car2.dateCreated
// }
// self.dataArray.append(contentsOf: sortedCars)
// }
}
}
This question already has answers here:
How to change a value of struct that is in array?
(2 answers)
Closed 1 year ago.
I'm trying to achieve a two way binding-like functionality.
I have a model with an array of identifiable Items, var selectedID holding a UUID of selected Item, and var proxy which has get{} that looks for an Item inside array by UUID and returns it.
While get{} works well, I can't figure out how to make proxy mutable to change values of selected Item by referring to proxy.
I have tried to implement set{} but nothing works.
import SwiftUI
var words = ["Aaaa", "Bbbb", "Cccc"]
struct Item: Identifiable {
var id = UUID()
var word: String
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(word: "One"), Item(word: "Two"), Item(word: "Three")]
#Published var selectedID: UUID?
var proxy: Item? {
set {
// how to set one property of Item?, but not the whole Item here?
}
get {
let index = items.firstIndex(where: { $0.id == selectedID })
return index != nil ? items[index!] : nil
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
// monitoring
MonitorkVue(model: model)
//selections
HStack {
ForEach(model.items.indices, id:\.hashValue) { i in
SelectionVue(item: $model.items[i], model: model)
}
}
}.padding()
}
}
struct MonitorkVue: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text(model.proxy?.word ?? "no proxy")
// 3rd: cant make item change by referring to proxy
// in order this to work, proxy's set{} need to be implemented somehow..
Button {
model.proxy?.word = words.randomElement()!
} label: {Text("change Proxy")}
}
}
}
struct SelectionVue: View {
#Binding var item: Item
#ObservedObject var model: Model
var body: some View {
VStack {
Text(item.word).padding()
// 1st: making selection
Button {
model.selectedID = item.id } label: {Text("SET")
}.disabled(item.id != model.selectedID ? false : true)
// 2nd: changing item affects proxy,
// this part works ok
Button {
item.word = words.randomElement()!
}label: {Text("change Item")}
}
}
}
Once you SET selection you can randomize Item and proxy will return new values.
But how to make it works the other way around when changing module.proxy.word = "Hello" would affect selected Item?
Does anyone knows how to make this two-way shortct?
Thank You
Here is a correction and some fix:
struct Item: Identifiable {
var id = UUID()
var word: String
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(word: "One"), Item(word: "Two"), Item(word: "Three")]
#Published var selectedID: UUID?
var proxy: Item? {
get {
if let unwrappedIndex: Int = items.firstIndex(where: { value in (selectedID == value.id) }) { return items[unwrappedIndex] }
else { return nil }
}
set(newValue) {
if let unwrappedItem: Item = newValue {
if let unwrappedIndex: Int = items.firstIndex(where: { value in (unwrappedItem.id == value.id) }) {
items[unwrappedIndex] = unwrappedItem
}
}
}
}
}
In my project i hold a large dict of items that are updated via grpc stream. Inside the app there are several places i am rendering these items to UI and i would like to propagate the realtime updates.
Simplified code:
struct Item: Identifiable {
var id:String = UUID().uuidString
var name:String
var someKey:String
init(name:String){
self.name=name
}
}
class DataRepository {
public var serverSymbols: [String: CurrentValueSubject<Item, Never>] = [:]
// method that populates the dict
func getServerSymbols(serverID:Int){
someService.fetchServerSymbols(serverID: serverID){ response in
response.data.forEach { (name,sym) in
self.serverSymbols[name] = CurrentValueSubject(Item(sym))
}
}
}
// background stream that updates the values
func serverStream(symbols:[String] = []){
someService.initStream(){ update in
DispatchQueue.main.async {
self.serverSymbols[data.id]?.value.someKey = data.someKey
}
}
}
}
ViewModel:
class SampleViewModel: ObservableObject {
#Injected var repo:DataRepository // injection via Resolver
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
func getItem(item:String) -> Item {
guard let item = repo.serverSymbols[item] else { return Item(name:"N/A")}
return ItemPublisher(item: item).data
}
}
class ItemPublisher: ObservableObject {
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:CurrentValueSubject<Item, Never>){
item
.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
Main View with subviews:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item: viewModel.getItem(item: item))
}
}
}
}
struct FavoriteCardView: View {
var item:Item
var body: some View {
VStack {
Text(item.name)
Text(item.someKey) // dynamic value that should receive the updates
}
}
}
I must've clearly missed something or it's a completely wrong approach, however my Item cards do not receive any updates (i verified the backend stream is active and serverSymbols dict is getting updated). Any advice would be appreciated!
I've realised i've made a mistake - in order to receive the updates i need to pass down the ItemPublisher itself. (i was incorrectly returning ItemPublisher.data from my viewModel's method)
I've refactored the code and make the ItemPublisher provide the data directly from my repository using the item key, so now each card is subscribing individualy using the publisher.
Final working code now:
class SampleViewModel: ObservableObject {
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
}
MainView and CardView:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item)
}
}
}
}
struct FavoriteCardView: View {
var itemName:String
#ObservedObject var item:ItemPublisher
init(_ itemName:String){
self.itemName = itemName
self.item = ItemPublisher(item:item)
}
var body: some View {
let itemData = item.data
VStack {
Text(itemData.name)
Text(itemData.someKey)
}
}
}
and lastly, modified ItemPublisher:
class ItemPublisher: ObservableObject {
#Injected var repo:DataRepository
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:String){
self.data = Item(name:item)
if let item = repo.serverSymbols[item] {
self.data = item.value
item.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
}
I tried to create a list of editable objects in SwiftUI. Here is my idea.
First of all, the editable item is as follows:
struct Item: Identifiable {
var id: UUID
var ItemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [
.init(id: .init(), ItemNum: 55),
.init(id: .init(), ItemNum: 57),
.init(id: .init(), ItemNum: 87)
]
}
After that I created a list that get data from the ItemStore:
struct ItemView: View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore
var body: some View {
NavigationView {
List {
ForEach(store.items.indexed(), id:\.1.id) {index, item in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
//.onAppear(perform: store.fetch) // want to fetch the data from the store whenever the list appear, however, no idea to perform the function?!
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
The editView:
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text(Text("Item Num: \(item.itemNum)"))
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
My question here:
I would like to fetch the data from 'store' onAppear. but it fails.
After I quit the app, all the previous data gone. How can I make them to keep inside my app, even the app is kill?
Your second question first: In terms of storing (persisting your data), you have many options. The easiest would be to store it in UserDefaults, which I'll show in my example. You could also choose to use CoreData, which would be more of a process to set up, but would give you a more robust solution later on. Many more options like Realm, Firebase, SQLite, etc. exist as well.
struct Item: Identifiable, Codable {
var id: UUID = UUID()
var itemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "savedItems")
}
}
}
let defaultValues : [Item] = [
.init(itemNum: 55),
.init(itemNum: 57),
.init(itemNum: 87)
]
func fetch() {
let decoder = JSONDecoder()
if let savedItems = UserDefaults.standard.object(forKey: "savedItems") as? Data,
let loadedItems = try? decoder.decode([Item].self, from: savedItems) {
items = loadedItems
} else {
items = defaultValues
}
}
}
struct ContentView : View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore = ItemStore()
var body: some View {
NavigationView {
List {
ForEach(Array(store.items.enumerated()), id:\.1.id) { (index,item) in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
.onAppear(perform: store.fetch)
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text("Item Num: \(item.itemNum)")
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
Regarding your first question, the reason that fetch failed is you had no fetch method. Plus, there was nothing to fetch, since the array of items just got populated upon creation of the ItemStore each time.
Notes:
Item now conforms to Codable -- this is what allows it to get transformed into a value that can be saved/loaded from UserDefaults
fetch is now called on onAppear.
Every time the data is changed, didSet is called, saving the new data to UserDefaults
There were a number of typos and things that just plain wouldn't compile in the original code, so make sure that the changes are reflected. Some of those include: enumerated instead of indexed in the ForEach, not calling Text(Text( with nested values, using the same capitalization of itemNum throughout, etc
Important: when testing this, make sure to give the simulator a few seconds after a change to save the data into UserDefaults before killing the app and opening it again.