View as a function pointer - swiftui

Is it possible to declare a View as a var in some way, as in below struct?
struct OptionViews {
var title: String
var imageName: String
var targetView: View
}
I want to use the above struct to present as an array of possible selections (as in a settings view) where if I click on an item a it should open the targetView. However, the above struct is not allowed due to "Protocol View can only be used as a generic constraint", is there a way around this or another way to accomplish this? I need to know which View should be opened when the specific item is selected, but if i cant specify the view as part of the item, that does not seem possible.
Thanks,
Marcus

I guess what #Asperi said in comment is right. Not sure what you exactly want but maybe you could make your struct generic like:
struct OptionViews<Content> where Content: View {
var title: String
var imageName: String
var targetView: Content
}
// example
let optionViews = OptionViews(title: "Titel",
imageName: "Image",
targetView: Image(systemName: "heart"))

Related

Binding<String> action tried to update multiple times per frame in SwiftUI

I have a VM that is implemented as follows:
LoginViewModel
class LoginViewModel: ObservableObject {
var username: String = ""
var password: String = ""
}
In my ContentView, I use the VM as shown below:
#StateObject private var loginVM = LoginViewModel()
var body: some View {
NavigationView {
Form {
TextField("User name", text: $loginVM.username)
TextField("Password", text: $loginVM.password)
Every time I type something in the TextField it shows the following message in the output window:
Binding<String> action tried to update multiple times per frame.
Binding<String> action tried to update multiple times per frame.
Binding<String> action tried to update multiple times per frame.
It is a message and not an error.
If I decorate my username and password properties with #Published then the message goes away but the body is rendered each time I type in the TextField.
Any ideas what is going on and whether I should use #Published or not. I don't think I will gain anything from putting the #Published attribute since this is a one-way binding and I don't want to display anything on the view once the username changes.
If I decorate my username and password properties with #Published then the message goes away
This is the correct solution. You need to use #Published on those properties because that is how SwiftUI gets notified when the properties change.
the body is rendered each time I type in the TextField
That is fine. Your body method is not expensive to compute.
I don't think I will gain anything from putting the #Published attribute since this is a one-way binding
You cannot be sure SwiftUI will work correctly (now or in future releases) if you don't use #Published. SwiftUI expects to be notified when the value of a Binding changes, even when a built-in SwiftUI component like TextField causes the change.
For the simple case - the state is kept in the same view or in a ModelSupport class, consists of strings or other primitive types, and there's only one of each, #Published will work fine.
I got this error with a model class containing an array of structs and using a List, and every time you type inside a TextField inside a list (or every time you select an item in a list), the view gets refreshed, and the error gets triggered.
I am thus using a DelayedTextField:
struct DelayedTextField: View {
var title: String = ""
#Binding var text: String
#State private var tempText: String = ""
var body: some View {
TextField(title, text: $tempText, onEditingChanged: { editing in
if !editing {
$text.wrappedValue = tempText
}
})
.onAppear {
tempText = text
}
}
}
and the binding update error is no more.

Observe Collection Results in Realm with Combine and SwiftUI

I am trying out this quick start for SwiftUI and Combine in order to try and understand how to connect my Realm database to Combine.
The example observes a RealmSwift.List and keeps a table populated with its data. This is is a linked list to a child class. I'm wondering how to observe a Results collection so I can keep track of any changes to an entire Realm class.
For example, let's say I have a Workspace class:
class Workspace: Object, ObjectKeyIdentifiable{
#objc dynamic var id = UUID().uuidString
#objc dynamic var name = ""
#objc dynamic var archived = false
}
In the state object, I can set up a Results<Workspace> variable like this:
class AppState: ObservableObject {
#Published var workspaces: Results<Workspace>?
var cancellables = Set<AnyCancellable>()
init(){
let realmPublisher = PassthroughSubject<Realm, Error>()
realmPublisher
.sink(receiveCompletion: { _ in }, receiveValue: { realm in
//Get the Results
self.workspaces = realm.objects(Workspace.self)
})
.store(in: &cancellables)
realmPublisher.send(try! Realm())
return
}
}
But when it comes time to observe the object, I can't because Results isn't an object (I assume).
struct ContentView: App {
#ObservedObject var state = AppState()
var view: some View {
ItemsView(workspaces: state.workspaces!)
}
var body: some Scene {
WindowGroup {
view.environmentObject(state)
}
}
}
struct ItemsView: View {
#ObservedObject var workspaces: Results<Workspace> //<!-- Error
var body: some View {
//...
}
}
Xcode gives a syntax error on the workspaces property:
Property type 'Results' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'
Is it possible to observe a set of Results just like we can have a notification listener on a collection of Results?
Technically, you could hook up a sink to state.workspaces (state.$workspaces.sink()), but in this case, I think you're overcomplicating the problem.
You already have an #ObservableObject in your ContentView (AppState) that is managing the results for you. So, change ItemsView to just take this as a parameter:
var workspaces: Results<Workspace>?
It doesn't need to be an #ObservedObject -- either way, whether it's getting observed in that view or it's parent view, it's going to get re-rendered. It does have to be optional here, since it's an optional value on your AppState, unless you want to keep passing it with the force unwrap (!), but that's generally a bad idea, since it'll crash if it ever is in fact nil.
Also, above, in your Realm code, make sure it's matching the tutorial that you were following. For example, you have Publisher.sink which should really be realmPublisher.sink
You are correct, Results is a struct, and therefore cannot be covered by #StateObject or #ObservedObject. Your workaround is suitable for now.
Once https://github.com/realm/realm-cocoa/pull/7045 is released, you will be able to use one of the new Realm property wrappers to embed your Results into the view directly. At the time of this posting, that would be #FetchRealmResults, but that is subject to change.

NavigationLink hides the Destination View, or causes infinite view updates

Let us consider the situation when you have ContentView and DestinationView. Both of them depend on some shared data, that typically lies inside the #ObservedObject var viewModel, that you pass from parent to child either via #EnvironmentObject or directly inside init().
The DestinationView in this case wants to enrich the viewModel by fetching some additional content inside .onAppear.
In this case, when using NavigationLink you might encounter the situation when the DestinationView gets into an update loop when you fetching content, as it also updates the parent view and the whole structure is redrawn.
When using the List you explicitly set the row's ids and thus view is not changed, but if the NavigationLink is not in the list, it would update the whole view, resetting its state, and hiding the DestinationView.
The question is: how to make NavigationLink update/redraw only when needed?
In SwiftUI the update mechanism compares View structs to find out whether they need to be updated, or not. I've tried many options, like making ViewModel Hashable, Equatable, and Identifiable, forcing it to only update when needed, but neither worked.
The only working solution, in this case, is making a NavigationLink wrapper, providing it with id for equality checks and using it instead.
struct NavigationLinkWrapper<DestinationView: View, LabelView: View>: View, Identifiable, Equatable {
static func == (lhs: NavigationLinkWrapper, rhs: NavigationLinkWrapper) -> Bool {
lhs.id == rhs.id
}
let id: Int
let label: LabelView
let destination: DestinationView // or LazyView<DestinationView>
var body: some View {
NavigationLink(destination: destination) {
label
}
}
}
Then in ContentView use it with .equatable()
NavigationLinkWrapper(id: self.viewModel.hashValue,
label: myOrdersLabel,
destination: DestinationView(viewModel: self.viewModel)
).equatable()
Helpful tip:
If your ContentView also does some updates that would impact the DestinationView it's suitable to use LazyView to prevent Destination from re-initializing before it's even on the screen.
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
P.S: Apple seems to have fixed this issue in iOS14, so this is only iOS13 related issue.

SwiftUI - How to create an edit view for each list view item based on #ObservedObject

I have created a simple example of the problem I'm facing. I have two views, ListView and EditView. ListView is observing the UsersViewModel (plural) which contain a list of user names.
Using NavigationLink I want to present a form where I can edit the user name and this is where I'm getting confused. I have created a UserViewModel (singular) which I have the EditView observing, but when I try to call the EditView passing the value from the ForEach loop, I get a type mismatch error as I am not passing a UserViewModel.
Maybe I am misunderstanding the observable object. I thought I could change the user name on the edit form, navigate back to the list view and I would see the change in the list.
struct ListView: View {
// Observe the Users View Model
#ObservedObject var usersViewModel = UsersViewModel()
var body: some View {
NavigationView {
List() {
ForEach(usersViewModel.users) { user in
// FAILS with cannot converted "user" to expected type userViewModel
NavigationLink (destination: EditView(userViewModel: user)) {
Text("Hello \(user.name)")
}
}
}
.navigationBarTitle("User List", displayMode: .inline)
.onAppear(){
usersViewModel.loadUsers()
}
The edit view
struct EditView: View {
#ObservedObject var userViewModel: UserViewModel
var body: some View {
TextField("User Name", text: $userViewModel.user)
cannot converted "user" to expected type userViewModel
You probably want to use the same UserViewModel in both views (assuming the user is a struct).
Change your EditView to expect a usersViewModel parameter in init:
struct EditView: View {
#ObservedObject var usersViewModel: UsersViewModel
and pass it in the parent view:
NavigationLink (destination: EditView(usersViewModel: usersViewModel))
Otherwise, as the user is probably a struct, you will modify the different copy of the user in the child view.
I have got around the issue by passing the usersViewModel to the editView rather than userViewModel as pawello2222 suggested, but obviously you need to know which element you want to edit.
So I have done this;
let users = usersViewModel.users.enumerated().map({ $0 })
NavigationView {
List() {
ForEach(users, id: \.element.id ) { index, user in
NavigationLink (destination: EditView(userViewModel: usersViewModel, index: index)) {
This seems long winded, surely having to enumerate the viewModel to provide the EditView with an index of which element to edit is not the correct way?
It works, but I would like to know the proper way

What does "\.self" actually do in swift/swiftUI?

I don't fully understand what exactly \.self does in the following code:
struct ContentView: View {
#State private var numbers = [Int]()
#State private var currentNumber = 1
var body: some View {
VStack {
List {
ForEach(numbers, id: \.self) {
Text("\($0)")
}
}
Button("Add Number") {
self.numbers.append(self.currentNumber)
self.currentNumber += 1
}
}
}
}
I'm following this: https://www.hackingwithswift.com/books/ios-swiftui/deleting-items-using-ondelete.
I have a really basic understanding of the language right now, so I may not understand high level explanations, so would prefer very simple explanations or analogies with verbose descriptions. I think it is setting the idfor each list item as each item in the numbers array? Correct me if wrong - but is each id being set as whatever Int is in each entry of the numbers array? If so, then what does \ actually do when typing \.self and what does .self actually do in combination with \?
. key paths. ForEach needs every object unique. give them unique ids with id:.self. if your objects are identifiable you dont need .self.
i wrote about this in medim if you want you can check out
id: .self tells Swift to use as unique id (keypath) the hash of the object. It explains why the name "self" is used. id: .self is especially useful for the basic Swift types like Integer and String. On the one the hand the developer cannot add to them an unique id. On the other hand all they are hashable so we can use id: .self. id: .self is useful not only for ForEach, but for List also.