I'm trying to practice using Supabase with SwiftUI as an alternative to Firebase. I have a table set up in my Supabase database and my code is identifying this and retrieving the data, but I'm having issues displaying it in a list once it's been retrieved.
I can print the data to the console window to show it has in fact been retrieved but the List doesn't seem to display anything on the simulator screen.
My view model contains this code:
let client = SupabaseClient(supabaseUrl: SupabaseURL, supabaseKey: SupabaseKey)
#Published var todos: [Todo] = []
// Retrieve all todo's
func getAllTodos() {
print("Retrieving all todo's from database")
let query = client.database.from("todos")
.select()
query.execute { results in
switch results {
case let .success(response):
let todos = try? response.decoded(to: [Todo].self)
self.todos = todos!
print("Todos: \(todos)")
case let .failure(error):
print(error.localizedDescription)
}
}
}
And my ContentView contains this code:
#State private var model = ContentViewModel()
#State var title = ""
#State var description = ""
var body: some View {
VStack {
TextField("Title", text: $title)
.padding(5)
.border(Color.black)
TextField("Description", text: $description)
.padding(5)
.border(Color.black)
Button("Create Todo") {
model.createTodo(title: title, description: description)
}
.padding(10)
Button("List todo's") {
model.getAllTodos()
}
.padding(10)
List(model.todos) { todo in
Text("\(todo.title): \(todo.description)")
Text("Done: \(String(describing: todo.isDone))")
}
.refreshable {
model.getAllTodos()
}
}
.padding(20)
.onAppear {
model.getAllTodos()
}
}
If I run the simulator, and click 'List todo's' once, the console shows this output:
Retrieving all todo's from database
2022-01-30 17:59:19.077191+0000 Supabase API 2[36461:1087300] [boringssl] boringssl_metrics_log_metric_block_invoke(151) Failed to log metrics
Todos: [Supabase_API_2.Todo(id: "56e81c25-2fbe-45da-ac57-5e0d92fdd9b4", title: "First Todo", description: "basic description", isDone: false), Supabase_API_2.Todo(id: "06b8e7bf-f990-4553-9c92-5a12a38d8e78", title: "Second todo", description: "another basic description", isDone: false)]
Retrieving all todo's from database
Todos: [Supabase_API_2.Todo(id: "56e81c25-2fbe-45da-ac57-5e0d92fdd9b4", title: "First Todo", description: "basic description", isDone: false), Supabase_API_2.Todo(id: "06b8e7bf-f990-4553-9c92-5a12a38d8e78", title: "Second todo", description: "another basic description", isDone: false)]
You can see it retrieves the todo's when the view loads, then again when I click the 'List todo's' button. So I can see it is correctly retrieving the data and storing in the 'todos' var, but for some reason won't display it in the list.
Any help with this would be really appreciated!
Assuming ContentViewModel is an ObservableObject (which it should be), it should be annotated with #StateObject (which is the property wrapper that allows the View to hook up to the objects publisher), not #State.
Related
#State var myDict: [String: Double] = [:]
var body: some View {
HStack(alignment: .top) {
Button(action: {
self.myDict[self.tag ?? ""] = amountValue
})
}
}
I have two user, when i select 1st user I add value to myDict, and when I select 2nd user I want to add another value to it (means myDict will have 2 object), but when i select 2nd user #state variable is refresh and have only one value of 2nd user. (means myDict have 1 object)
is there any way so that I can have both the value in dict not only one?
actually, this can happen that for both users self.tag is nil and ("") is used as a key for the dictionary
self.myDict[self.tag ?? ""] = amountValue
and if this happens you may want to make sure that self.tag is not nil
I have created a test environment just like your case but I have
import SwiftUI
struct swiftUIDoubt1: View {
#State var myDict: [String: Double] = [:]
var body: some View {
VStack{
Button(action: {
myDict["Hello, World!"] = 9.99999
print(myDict)
}, label: {
Text("\(myDict["Hello, World!"] ?? 0)")
.padding()
})
Button(action: {
myDict["bye, World!"] = 1.11111
print(myDict)
}, label: {
Text("\(myDict["bye, World!"] ?? 0)")
.padding()
})
}
.onAppear(perform: {
print(myDict)
})
}
}
now as you can see when my screen will appear my dictionary should be empty and an empty dictionary must be printed, as you can see in the image
console log when the app is first opened
when I click first button single item will be added and you can see in console log
and when I will click on the second button you can see I have two different items in my dictionary
let success = "two different items has been added to dictionary"
as #state variable to managed by swift UI and will not change with ui update
Small example : The add in the subview update the dictionnaire for the desired user in parent view
import SwiftUI
struct UpdateUsersDict: View {
#State var myDict: [String: Double] = [:]
#State var amount: Double = 100
var body: some View {
VStack {
HStack {
OneUserView(myDict: $myDict, amount: amount, tag: "user 1")
OneUserView(myDict: $myDict, amount: amount, tag: "user 2")
OneUserView(myDict: $myDict, amount: amount, tag: "user 3")
}
HStack {
Text("\(myDict["user 1"] ?? -1)")
Text("\(myDict["user 2"] ?? -1)")
Text("\(myDict["user 3"] ?? -1)")
}
}
}
}
struct OneUserView: View {
#Binding var myDict: [String: Double]
#State var amount: Double
var tag: String
var body: some View {
HStack(alignment: .top) {
Button(tag, action: {
self.myDict[self.tag] = amount
})
}
}
}
struct UpdateUserDict_Previews: PreviewProvider {
static var previews: some View {
UpdateUsersDict()
}
}
I have a CoreData plants entity, a model with a text value, a masterView and a simple detailView
Model:
class TextItem: Identifiable {
var id: String
var text: String = ""
init() {
id = UUID().uuidString
}
}
class RecognizedContent: ObservableObject {
#Published var items = [TextItem]()
}
List in MasterView:
List(recognizedContent.items, id: \.id) { textItem in
NavigationLink(destination: TextPreviewView(text: textItem.text)
.environment(\.managedObjectContext, viewContext)) {
if let item = plants.first(where: {$0.wrappedID == textItem.text}) {
PlantRow(item: item)
}
Text(String(textItem.text))
}
struct TextPreviewView: View {
var text: String
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Plant.timestamp, ascending: true)],
animation: .default)
private var plants: FetchedResults<Plant>
var body: some View {
VStack {
Text(text)
.font(.body)
.padding()
ForEach(plants.filter({$0.wrappedID.range(of: String(text), options: .caseInsensitive) != nil}), id: \.self) { plant in
PlantRow(item: plant)
Text(plant.visualId ?? "")
}
}
}
}
plants has a wrappedID (String) with a certain value ("Benjamin" in this case). When I manually replace String(text) in the ForEach filter with "Benjamin" it filters out the correct value and shows the row. Whenever I let text be filled in from the Model, it doesn't find anything. When I, on ButtonClick, cast the text in recognizedContent.items.text to a #State variable and feed that into the filter, it immediately shows the PlantRows.
I've also changed the ForEaches to all possible ways. Whenever I feed in a 'normal' string, the filter works. However, Text(String(textItem.text)) also always renders and shows correct string.
Is it a problem in the ObservedObject?
I am using coredata to save information. This information populates a picker, but at the moment there is no information so the picker is empty. The array is set using FetchedRequest.
#FetchRequest(sortDescriptors: [])
var sources: FetchedResults<Source>
#State private var selectedSource = 0
This is how the picker is setup.
Picker(selection: $selectedSource, label: Text("Source")) {
ForEach(0 ..< sources.count) {
Text(sources[$0].name!)
}
}
There is also a button that displays another sheet and allows the user to add a source.
Button(action: { addSource.toggle() }, label: {
Text("Add Source")
})
.sheet(isPresented: $addSource, content: {
AddSource(showSheet: $addSource)
})
If the user presses Add Source, the sheet is displayed with a textfield and a button to add the source. There is also a button to dismiss the sheet.
struct AddSource: View {
#Environment(\.managedObjectContext) var viewContext
#Binding var showSheet: Bool
#State var name = ""
var body: some View {
NavigationView {
Form {
Section(header: Text("Source")) {
TextField("Source Name", text: $name)
Button("Add Source") {
let source = Source(context: viewContext)
source.name = name
do {
try viewContext.save()
name = ""
} catch {
let error = error as NSError
fatalError("Unable to save context: \(error)")
}
}
}
}
.navigationBarTitle("Add Source")
.navigationBarItems(trailing: Button(action:{
self.showSheet = false
}) {
Text("Done").bold()
.accessibilityLabel("Add your source.")
})
}
}
}
Once the sheet is dismissed, it goes back to the first view. The picker in the first view is not updated with the newly added source. You have to close it and reopen. How can I update the picker once the source is added by the user? Thanks!
The issue is with the ForEach signature you're using. It works only for constant data. If you want to use with changing data, you have to use something like:
ForEach(sources, id: \Source.name.hashValue) {
Text(verbatim: $0.name!)
}
Note that hashValue will not be unique for two entity objects with the same name. This is just an example
so I'm having a bit of an issue here I'm hoping is easy to fix, just can't figure it out at the moment. I'm running a loop through some CoreData info (posts) and returning a grid of images, I want to be able to click these images and open up a fullScreenCover of the DetailView with the correct info in it. With the current code, the DetailView always shows the data from the first post. If I change it from a Button to a NavigationLink NavigationLink(destination: DetailView(post: post)), as commented out in the code, it works perfectly, but doesn't give me the fullScreenCover behaviour I would like. What am I doing wrong here? Thanks in advance!
#FetchRequest(entity: Post.entity(), sortDescriptors: []) var posts: FetchedResults<Post>
enum ActiveSheet: Identifiable {
case detail, addNew
var id: Int {
hashValue
}
}
#State var activeSheet: ActiveSheet?
var body: some View {
ForEach(posts.reversed(), id: \.self) { post in
VStack {
Button(action: { activeSheet = .detail }){
//NavigationLink(destination: DetailView(post: post)){
ZStack {
Image(uiImage: UIImage(data: post.mainImage ?? self.image)!)
VStack {
Text("\(post.title)")
Text("\(post.desc)")
}
}
}
.fullScreenCover(item: $activeSheet) { item in
switch item {
case .detail:
DetailView(post: post)
case .addNew:
AddNewView()
}
}
}
}
}
I've made the array of posts static for now instead of coming from Core Data and mocked the objects/structs so that I could test easily, but the principal should stay the same:
struct ContentView : View {
//#FetchRequest(entity: Post.entity(), sortDescriptors: []) var posts: FetchedResults<Post>
var posts : [Post] = [Post(title: "1", desc: "desc1"),
Post(title: "2", desc: "desc2"),
Post(title: "3", desc: "desc3")]
enum ActiveSheet: Identifiable {
case detail(post: Post)
case addNew
var id: UUID {
switch self {
case .detail(let post):
return post.id
default:
return UUID()
}
}
}
#State var activeSheet: ActiveSheet?
var body: some View {
ForEach(posts.reversed(), id: \.self) { post in
VStack {
Button(action: { activeSheet = .detail(post: post) }){
ZStack {
//Image(uiImage: UIImage(data: post.mainImage ?? self.image)!)
VStack {
Text("\(post.title)")
Text("\(post.desc)")
}
}
}
}
.fullScreenCover(item: $activeSheet) { item in
switch item {
case .detail(let post):
DetailView(post: post)
case .addNew:
AddNewView()
}
}
}
}
}
struct DetailView : View {
var post: Post
var body : some View {
Text("Detail \(post.id)")
}
}
struct AddNewView : View {
var body : some View {
Text("add")
}
}
struct Post : Hashable {
var id = UUID()
var title : String
var desc : String
}
The basic idea is that instead of creating the fullScreenCover on first render, you should create it in based on the activeSheet so that it gets created dynamically. You were on the right track using item: and activeSheet already -- the problem was it wasn't tied to the actual post, since you were just using the button to set activeSheet = .detail.
I've added an associated property to case detail that allows you to actually tie a post to it. Then, in fullScreenCover you can see that I use that associated value when creating the DetailView.
You may have to make slight adjustments to fit your Post model, but the concept will remain the same.
Using Xcode 11.0, Beta 5.
I have a List driven from an array of model objects in an observed view model. Each item in the List has a NavigationLink with a destination detail view/view model that accepts the model as an argument.
The user can tap a bar button above the list to add a new item which is added to the view model's array and the List is therefore reloaded with the new item displayed.
The issue I cannot solve is how to select that new item in the list and therefore display the detail view without needing the user to select it manually. (This is an iPad app with split screen view, hence the reason to want to select it)
I've tried using a NavigationLink programatically, but can't seem to get anything to work. I looked at the selection argument for the List but that also requires the list to be in edit mode, so that's no good.
Any suggestions are very welcome!
The following solution uses the selection attribute of NavigationLink. One problem here is that only items currently visible get rendered, so selecting a row further down does nothing.
import SwiftUI
struct Item: Identifiable {
let id = UUID()
var title: String
var content: String
}
struct ItemOverview: View {
let item: Item
var body: some View {
Text(item.title)
}
}
struct ItemDetailsView: View {
let item: Item
var body: some View {
VStack{
Text(item.title).font(.headline)
Divider()
Text(item.content)
Spacer()
}
}
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
struct ListSelectionView: View {
#State var items: [Item] = [
Item(title: "Hello", content: "Content World"),
Item(title: "Hey", content: "Content Du"),
]
#State var selection: UUID? = nil
func createItem() {
let newItem = Item(title: randomString(length: 3), content: randomString(length: 10))
self.selection = newItem.id
self.items.append(newItem)
}
var body: some View {
VStack{
NavigationView{
List(items) { item in
NavigationLink(destination: ItemDetailsView(item: item), tag: item.id, selection: self.$selection, label: {
Text(item.title)
})
}
}
Button(action: {
self.createItem()
}) { Text("Add and select new item") }
Divider()
Text("Current selection2: \(String(selection?.uuidString ?? "not set"))")
}
}
}
A second problem is that changing the $selection makes Modifying state during view update appear.
Third problem is that after manual selection the shading stays on the same item until again changed by hand.
Result
Programmatic selection is not really usable for now if you want to select a link not initialized yet (not visible?).
Further ideas
One might look into tags a little bit more.
Another option could be paging where all items of the current page are visible.
One could also use list selection and show details based on that.