Implementing business and other logic in SwiftUI - swiftui

(This is my first SwiftUI project; please be kind if this is a stupid question.)
I have a collection of objects which are displayed in a Picker. The picker selection is $selectedIndex, where
#State private var selectedIndex: Int = 0
I also have a
#State private var opts: OptsStruct = OptsStruct()
where elements of the OptsStruct structure are bound to SwiftUI views. The value of opts needs to change when the selectedIndex changes, because the opts property is the option shown in and selected by the Picker. (Also, I want to save the current value of selectedIndex in UserDefaults.) The problem is that I don't understand how to express these actions in SwiftUI.
I tried
#State private var selectedIndex: Int = 0 {
mutating didSet {
// save selectedIndex to UserDefaults
opts = f(selectedIndex)
}
but this causes a Segmentation Fault.
Where is the 'correct' place to put this logic. (And in general, can someone suggest some reading on how to connect changes to SwiftUI #States with general business logic.)
Thanks,
Rick

The idea of a #State variable is for it to be the single source of truth (wikipedia). This means that one variable should be the only thing that contains the "state" of your picker. In this case, I suggest using this:
$opts.selectionIndex
as the Binding for your picker. selectionIndex would then be a Int property of your OptsStruct type.

Related

How to initialize state variable with core data model

I don't know how to initialize a CoreData model in my ContentView:
This is the declaration line so far:
#State var selectedFriend: Friend
I want to use the type Friend in a picker but I don't know how to setup a custom picker so it works with a core data model.
Any thoughts or suggestions?
Thank you.
If you fetch the data with #FetchRequest and you want to display the data in a picker (no empty selection) my suggestion is to work with indices. A scalar value type can be initialised directly.
#State private var selectedFriendIndex = 0
...
Picker("Friend", selection: $selectedFriendIndex) {
ForEach(0..<friends.count, id: \.self) { index in
Text(friends[index].name)
}
}

#State variable getting reset for no reason [ios 15 SwiftUI]

I am using a #State variable to store a tag of some navigation Links.
But in ios 15, I notice that whenever I go to background, the state variable is getting reset, which causes that when I go back into the app, the view get popped for no reason.
#State private var userNavigateSelection: String? = nil
NavigationLink(destination: TestView(), tag: TestTag, selection: $userNavigateSelection) { EmptyView() }
I used a onChange method to monitor the changing of "userNavigateSelection", everytime I go into background. "userNavigationSelection" will be changed to nil. I have no idea why.
Perhaps your view containing #State variable is removed from view hierarchy after coming back to foreground. Try moving this variable higher in hierarchy and pass to view containing NavigationLink with #Binding.
Check: https://www.ralfebert.com/swiftui/state-lifetime/

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.

Implementing Swift UI Picker

What is the correct way to implement a Picker component with specific logic within a Section element?
I would like to have each type displayed in a separate row.
var types = ["Books", "Films", "Music"]
var body: some View {
Form {
Section(header: Text("Type")) {
TextField("Type", text: $newCategoryType)
// Picker
}
}
}
First you must have a #State property that can be updated based on what selection the user makes, say in this case we have this
#State private var selectedType = "Books"
Then you will implement a Picker SwiftUI struct as follows
Picker("Please choose a type", selection: $selectedType) {
ForEach(types, id: \.self) {
Text($0)
}
}
Note that the \.self is really important for ForEach to distinguish between each element inside the list, without which the Picker won't perform the selection action correctly.
The above is enough for doing the job of displaying each option as a row since that is the default behaviour of ForEach
Additionally if you want to customise the look and feel of the picker
you would like to see .pickerStyle() view modifier, for which the docs and examples are mentioned
Also
Tip: Because pickers in forms have this navigation behavior, it’s important you present them in a NavigationView on iOS otherwise you’ll find that tapping them doesn’t work. This might be one you create directly around the form, or you could present the form from another view that itself was wrapped in a NavigationView.

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.