Type 'Void' cannot conform to 'View' | Swift - ios

I'm trying to create a List with data from my firebase reali-time database but i'm getting this error on the List line:
The error:
Type 'Void' cannot conform to 'View'
My code:
struct ActiveGoalsView: View {
#State var goals = ["finish this project"]
#State var ref = Database.database().reference()
var body: some View {
NavigationView {
List {
ref.child("users").child(Auth.auth().currentUser?.uid ?? "noid").child("goals").observeSingleEvent(of: .value) { snapshot in
for snap in snapshot.children {
Text(snap.child("title").value)
}
}
}.navigationBarHidden(true)
}
}
}
struct ActiveGoalsView_Previews: PreviewProvider {
static var previews: some View {
ActiveGoalsView()
}
}

You can't use imperative code like observeSingleEvent in the middle of your view hierarchy that doesn't return a View. As a commenter suggested, you'd be better off moving your asynchronous code outside of the body (I'd recommend to an ObservableObject). Here's one solution (see inline comments):
class ActiveGoalsViewModel : ObservableObject {
#Published var children : [String] = []
private var ref = Database.database().reference()
func getChildren() {
ref.child("users").child(Auth.auth().currentUser?.uid ?? "noid").child("goals").observeSingleEvent(of: .value) { snapshot in
self.children = snapshot.children.map { snap in
snap.child("title").value //you may need to use ?? "" if this returns an optional
}
}
}
}
struct ActiveGoalsView: View {
#State var goals = ["finish this project"]
#StateObject private var viewModel = ActiveGoalsViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.children, id: \.self) { child in //id: \.self isn't a great solution here -- you'd be better off returning an `Identifiable` object, but I have no knowledge of your data structure
Text(child)
}
}.navigationBarHidden(true)
}.onAppear {
viewModel.getChildren()
}
}
}

Related

SwiftUI: Issue with data binding for passing back the updated value to caller

I have 2 views where the
first view passes list of items and selected item in that to second view and
second view returns the updated selected item if user changes.
I am getting error 'Type of expression is ambiguous without more context' when i am sending the model property 'idx'.
//I cant make any changes to this model so cant confirm it with ObservableObject or put a bool property like 'isSelected'
class Model {
var idx: String?
....
}
class FirstViewModel: ObservableObject {
var list: [Model]
#Published var selectedModel: Model?
func getSecondViewModel() -> SecondViewModel {
let vm2 = SecondViewModel( //error >> Type of expression is ambiguous without more context
list: list,
selected: selectedModel?.idx // >> issue might be here but showing at above line
)
return vm2
}
}
struct FirstView: View {
#ObservableObject firstViewModel: FirstViewModel
var body: some View {
..
.sheet(isPresented: $showView2) {
NavigationView {
SecondView(viewModel: firstViewModel.getSecondViewModel())
}
}
..
}
}
class SecondViewModel: ObservableObject {
var list: [Model]
#Published var selected: String?
init(list: [Model], selected: Published<String?>) {
self.list = list
_selected = selected
}
func setSelected(idx: String) {
self.selected = idx
}
}
struct SecondView: View {
#ObservableObject secondViewModel: SecondViewModel
#Environment(\.presentationMode) var presentationMode
var body: some View {
...
.onTapGesture {
secondViewModel.setSelected(idx: selectedIndex)
presentationMode.wrappedValue.dismiss()
}
...
}
}
In case if I am sending 'Model' object directly to the SecondViewModel its working fine. I need to make changes the type and couple of other areas and instantiate the SecondViewModel as below
let vm2 = SecondViewModel(
list: list,
selected: _selectedModel
)
Since I need only idx I don't want to send entire model.
Also the reason for error might be but not sure the Model is #Published and the idx is not.
Any help is appreciated
Here is some code, in keeping with your original code that allows you to
use the secondViewModel as a nested model.
It passes firstViewModel to the SecondView, because
secondViewModel is contained in the firstViewModel. It also uses
firstViewModel.objectWillChange.send() to tell the model to update.
My comment is still valid, you need to create only one SecondViewModel that you use. Currently, your func getSecondViewModel() returns a new SecondViewModel every time you use it.
Re-structure your code so that you do not need to have nested ObservableObjects.
struct Model {
var idx = ""
}
struct ContentView: View {
#StateObject var firstMdl = FirstViewModel()
var body: some View {
VStack (spacing: 55){
FirstView(firstViewModel: firstMdl)
Text(firstMdl.secondViewModel.selected ?? "secondViewModel NO selected data")
}
}
}
class FirstViewModel: ObservableObject {
var list: [Model]
#Published var selectedModel: Model?
let secondViewModel: SecondViewModel // <-- here only one source of truth
// -- here
init() {
self.list = []
self.selectedModel = nil
self.secondViewModel = SecondViewModel(list: list, selected: nil)
}
// -- here
func getSecondViewModel() -> SecondViewModel {
secondViewModel.selected = selectedModel?.idx
return secondViewModel
}
}
class SecondViewModel: ObservableObject {
var list: [Model]
#Published var selected: String?
init(list: [Model], selected: String?) { // <-- here
self.list = list
self.selected = selected // <-- here
}
func setSelected(idx: String) {
selected = idx
}
}
struct FirstView: View {
#ObservedObject var firstViewModel: FirstViewModel // <-- here
#State var showView2 = false
var body: some View {
Button("click me", action: {showView2 = true}).padding(20).border(.green)
.sheet(isPresented: $showView2) {
SecondView(firstViewModel: firstViewModel)
}
}
}
struct SecondView: View {
#ObservedObject var firstViewModel: FirstViewModel // <-- here
#Environment(\.dismiss) var dismiss
#State var selectedIndex = "---> have some data now"
var body: some View {
Text("SecondView tap here to dismiss").padding(20).border(.red)
.onTapGesture {
firstViewModel.objectWillChange.send() // <-- here
firstViewModel.getSecondViewModel().setSelected(idx: selectedIndex) // <-- here
// alternatively
// firstViewModel.secondViewModel.selected = selectedIndex
dismiss()
}
}
}

SwiftUI - Should you use `#State var` or `let` in child view when using ForEach

I think I've a gap in understanding what exactly #State means, especially when it comes to displaying contents from a ForEach loop.
My scenario: I've created minimum reproducible example. Below is a parent view with a ForEach loop. Each child view has aNavigationLink.
// Parent code which passes a Course instance down to the child view - i.e. CourseView
struct ContentView: View {
#StateObject private var viewModel: ViewModel = .init()
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor) {
CourseView(course: course, viewModel: viewModel)
}
}
}
}
}
}
class ViewModel: ObservableObject {
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
}
struct Course: Identifiable {
var id: String = UUID().uuidString
var name: String
var instructor: String
}
Actual Dilemma: I've tried two variations for the CourseView, one with let constant and another with a #State var for the course field. Additional comments in the code below.
The one with the let constant successfully updates the child view when the navigation link is open. However, the one with #State var doesn't update the view.
struct CourseView: View {
// Case 1: Using let constant (works as expected)
let course: Course
// Case 2: Using #State var (doesn't update the UI)
// #State var course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Edit Instructor", action: editInstructor)
}
}
// Case 1: It works and UI gets updated
// Case 2: Doesn't work as is.
// I've to directly update the #State var instead of updating the clone -
// which sometimes doesn't update the var in my actual project
// (that I'm trying to reproduce). It definitely works here though.
private func editInstructor() {
let instructor = course.instructor == "Bob" ? "John" : "Bob"
var course = course
course.instructor = instructor
save(course)
}
// Simulating a database save, akin to something like GRDB
// Here, I'm just updating the array to see if ForEach picks up the changes
private func save(_ courseToSave: Course) {
guard let index = viewModel.courses.firstIndex(where: { $0.id == course.id }) else {
return
}
viewModel.courses[index] = courseToSave
}
}
What I'm looking for is the best practice for a scenario where looping through an array of models is required and the model is updated in DB from within the child view.
Here is a right way for you, do not forget that we do not need put logic in View! the view should be dummy as possible!
struct ContentView: View {
#StateObject private var viewModel: ViewModel = ViewModel.shared
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor, destination: CourseView(course: course, viewModel: viewModel))
}
}
}
}
}
struct CourseView: View {
let course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Update Instructor", action: { viewModel.update(course) })
}
}
}
class ViewModel: ObservableObject {
static let shared: ViewModel = ViewModel()
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
func update(_ course: Course) {
guard let index = courses.firstIndex(where: { $0.id == course.id }) else {
return
}
courses[index] = Course(name: course.name, instructor: (course.instructor == "Bob") ? "John" : "Bob")
}
}
struct Course: Identifiable {
let id: String = UUID().uuidString
var name: String
var instructor: String
}

SwiftUI MVVM Binding List Item

I am trying to create a list view and a detailed screen like this:
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
LazyVStack {
// https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
ForEach(viewModel.items.identifiableIndicies) { index in
MyListItemView($viewModel.items[index])
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = []
...
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item), label: {
...
})
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
#StateObject var viewModel: MyListViewItemDetailModel
init(item: Binding<Item>) {
viewModel = MyListViewItemDetailModel(item: item)
}
var body: some View {
...
}
}
class MyListViewItemDetailModel: ObservableObject {
var item: Binding<Item>
...
}
I am not sure what's wrong with it, but I found that item variables are not synced with each other, even between MyListItemDetailView and MyListItemDetailViewModel.
Is there anyone who can provide the best practice and let me know what's wrong in my implmentation?
I think you should think about a minor restructure of your code, and use only 1
#StateObject/ObservableObject. Here is a cut down version of your code using
only one StateObject source of truth:
Note: AFAIK Binding is meant to be used in View struct not "ordinary" classes.
PS: what is identifiableIndicies?
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Item: Identifiable {
let id = UUID().uuidString
var name: String = ""
}
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
var body: some View {
LazyVStack {
ForEach(viewModel.items.indices) { index in
MyListItemView(item: $viewModel.items[index])
}
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = [Item(name: "one"), Item(name: "two")]
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item)){
Text(item.name)
}
}
}
class MyAPIModel {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your fetching here
completion(Item(name: "new data from api"))
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
let myApiModel = MyAPIModel()
var body: some View {
VStack {
Button(action: fetchNewData) {
Text("Fetch new data")
}
TextField("edit item", text: $item.name).border(.red).padding()
}
}
func fetchNewData() {
myApiModel.fetchItemData() { itemData in
item = itemData
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyListView()
}.navigationViewStyle(.stack)
}
}
EDIT1:
to setup an API to call some functions, you could use something like this:
class MyAPI {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your stuff
}
}
and use it to obtain whatever data you require from the server.
EDIT2: added some code to demonstrate the use of an API.

SwiftUI NavigationView pops by itself

it's very strange issue as I cannot reproduce in isolated code, but I hope that someone may think about the reason. I have a view, let's say ContentView that has its ContentViewModel that is ObservedObject, and then there's another View ContentView2. And we have NavigationView in ContentView that wraps navigation link to ContentView2. And it's a bit weird, but when we do some changes that affect ContentViewModel, then NavigationView pops ContentView2 so that we end up in initial ContentView, but we didn't do anything like dismissing ContentView2 or tapping back button. I have a similar code to the one used in my project, but please note that in this code everything works fine:
func qrealm() -> Realm {
return try! Realm(configuration: .init( inMemoryIdentifier: "yw"))
}
class SomeRObj: Object {
#objc dynamic var name: String = ""
convenience init(name: String) {
self.init()
self.name = name
}
static var instance: SomeRObj {
return qrealm().objects(SomeRObj.self).first!
}
}
struct SomeRObjWrapped: Hashable {
var obj: SomeRObj
var prop: Int
}
class ContentViewModel: ObservableObject {
#Published var someRObj: [SomeRObjWrapped] = []
var any: Any?
init() {
let token = qrealm().objects(SomeRObj.self).observe { changes in
switch changes {
case let .initial(data), let .update(data, deletions: _, insertions: _, modifications: _):
let someObjs = data.map { SomeRObjWrapped(obj: $0, prop: Int.random(in: 1..<50)) }
self.someRObj = Array(someObjs)
default: break
}
}
self.any = token
}
}
struct ContentView: View {
#ObservedObject var model: ContentViewModel = ContentViewModel()
var body: some View {
NavigationView {
VStack {
ForEach(model.someRObj, id: \.self) { obj in
Heh(obj: obj.obj, pr: obj.prop)
}
NavigationLink(destination: ContentView2()) {
Text("Link")
}
}
}
}
}
struct Heh: View {
var obj: SomeRObj
var pr: Int
var body: some View {
Text("\(obj.name) \(pr)")
}
}
struct ContentView2: View {
var body: some View {
Button(action: { try! qrealm().write {
let elem = qrealm().objects(SomeRObj.self).randomElement()
elem?.name = "jotaro kujo"
}
}, label: { Text("Toggle") })
}
}
You can replace \.self with \.id:
ForEach(model.someRObj, id: \.id) { obj in
Heh(obj: obj.obj, pr: obj.prop)
}
Then every object will be identified by id and the ForEach loop will only refresh when the id is changed.
Thanks to pawello2222, I found the real reason behind it. I had a NavigationLink inside my List, so that each time there was a change NavigationLink is redrawn and it's state refreshed. I hope that it will be helpfull to someone, and the solution as pawello2222 wrote before is to identify view by id parameter.

How to use Dictionary as #Binding var in SwiftUI

I will need to display a collapsed menu in SwiftUI, it is possible to pass one single bool value as binding var to subviews but got stuck when trying to pass that value from a dictionary.
see code below:
struct MenuView: View {
#EnvironmentObject var data: APIData
#State var menuCollapsed:[String: Bool] = [:]
#State var isMenuCollapsed = false;
// I am able to pass self.$isMenuCollapsed but self.$menuCollapsed[menuItem.name], why?
var body: some View {
if data.isMenuSynced {
List() {
ForEach((data.menuList?.content)!, id: \.name) { menuItem in
TopMenuRow(dataSource: menuItem, isCollapsed: self.$isMenuCollapsed)
.onTapGesture {
if menuItem.isExtendable() {
let isCollapsed = self.menuCollapsed[menuItem.name]
self.menuCollapsed.updateValue(!(isCollapsed ?? false), forKey: menuItem.name)
} else {
print("Go to link:\(menuItem.url)")
}
}
}
}
}else {
Text("Loading...")
}
}
}
in ChildMenu Row:
struct TopMenuRow: View {
var dataSource: MenuItemData
#Binding var isCollapsed: Bool
var body: some View {
ChildView(menuItemData)
if self.isCollapsed {
//display List of child data etc
}
}
}
}
If I use only one single bool as the binding var, the code is running ok, however, if I would like to use a dictionary to store each status of the array, it has the error of something else, see image blow:
if I use the line above, it's fine.
Any idea of how can I fix it?
Thanks
How to use dictionary as a storage of mutable values with State property wrapper?
As mentioned by Asperi, ForEach requires that source of data conforms to RandomAccessCollection. This requirements doesn't apply to State property wrapper!
Let see one of the possible approaches in the next snippet (copy - paste - run)
import SwiftUI
struct ContentView: View {
#State var dict = ["alfa":false, "beta":true, "gamma":false]
var body: some View {
List {
ForEach(Array(dict.keys), id: \.self) { (key) in
HStack {
Text(key)
Spacer()
Text(self.dict[key]?.description ?? "false").onTapGesture {
let v = self.dict[key] ?? false
self.dict[key] = !v
}.foregroundColor(self.dict[key] ?? false ? Color.red: Color.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the following result

Resources