Redux conform Bindable for SwiftUI 2.0 - swift

I've been implementing Redux in my SwiftUI Project successfully but struggle when it comes to handling bindings properly. I want to use the binding functionality of SwiftUI while also storing the information in the Redux State.
As you see this kind of contradicts itself since the state can't be bound two-way.
This is my current code
#State var tab: Tab = .tab1
TabView(selection: $tab) { ... }
Ideally it should be comfortably usable like this if it's possible. I am also open to other ideas, that's just what i initially came up with - it's far from perfect.
#State var store = ReduxStore(...)
TabView(selection: $store.state.tab) { ... }

I figured out how to handle such cases. The solution for me is a Binding like:
let tabBinding = Binding<Tab> (
get: { self.store.state.currentTab }, // return the value from the state
set: { self.store.dispatch(action: NavigationAction.updateTab(tab: $0)) } // send the action here
)
Although this probably could be beautified it works, is pretty straight forward and solves the problem of working with Bindings in Redux.

Related

Sorting #FetchRequest (not dynamically) with SwiftUI

I tried the entire day to find the simplest solution to sort a #FetchRequest without any success!
I have tried this little modification:
#AppStorage("sortTitle") private var sortTitle = true
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [ sortTitle ? SortDescriptor(\.title) : SortDescriptor(\.date) ]) private var items: FetchedResults<Item>
And of course it doesn't work. Actually, I'm looking for something very simple because this isn't a dynamic sorting ; it's more like a one-time sorting that can be done by toggling sortTitle from the Settings screen.
There's of course one online solution (on the link below) but I'm not good enough to understand it correctly at the moment!
https://www.youtube.com/watch?v=O4043RVjCGU
Thanks, in advance, for your feedback. :)
This is worth a shot but to be honest #FetchRequest seems to be designed for a a ContentView that is only init once which is not the way SwiftUI is supposed to work.
.onAppear {
items.sortDescriptors = sortTitle ? [SortDescriptor(\.title)] : [SortDescriptor(\.date)]
}
.onChange(of: sortTitle) { newSortTitle in
items.sortDescriptors = sortTitle ? [SortDescriptor(\.title)] : [SortDescriptor(\.date)]
}
The flaw is that if the View containing this code is re-init, the change made to the sortDescriptors is lost.

How to have scene-specific state in SwiftUI?

I'm creating a macOS application that allows you to have multiple windows open. Each one of those windows (or scenes) need their own state, as the document/text/other states of that window are bound to it.
I also want to get that state in the .commands modifier so I can use the state to add keybindings.
The following I tried does not quite work, as all the state is global across all open windows (i.e. keybindings and text affect all instances of the application, while they must be different):
import SwiftUI
#main
struct QCCApp: App {
#StateObject var compiler = UIStateController()
var body: some Scene {
DocumentGroup(newDocument: QCCDocument()) { file in
ContentView()
// A function I've written that combines the file into state
.environmentObject(compiler.attachDocument(file))
}
.commands {
// Able to get the state to the commands area
CommandDelegate(compiler: compiler)
}
Settings {
SettingsView()
}
}
}
The following also doesn't quite work, but the behaviour of the document state is correct. The keybindings can't really be implemented this way:
import SwiftUI
#main
struct QCCApp: App {
var body: some Scene {
DocumentGroup(newDocument: QCCDocument()) { file in
// I implemented an initialiser instead of the `.attachDocument` method
let compiler = UIStateController(targetting: file)
ContentView()
.environmentObject(compiler)
}
.commands {
// I can't reach the `compiler` variable
CommandDelegate(compiler: /* compiler */)
}
Settings {
SettingsView()
}
}
}
Is there a way to have the keybindings specific to one app instance/scene/window? If so, how does one implement such a thing? In the first example, pressing a keybind would affect both windows (and the same goes for other state).

How can I prevent SwiftUI from reinitializing my wrapped property just like it does with #StateObject?

From what I've read whenever you instantiate an object yourself in your view, you should use #StateObject instead of #ObservedObject. Because apparently if you use #ObservedObject, SwiftUI might decide in any moment to throw it away and recreate it again later, which could be expensive for some objects. But if you use #StateObject instead then apparently SwiftUI knows not to throw it away.
Am I understanding that correctly?
My question is, how does #StateObject communicate that to SwiftUI? The reason I'm asking is because I've made my own propertyWrapper which connects a view's property to a firebase firestore collection and then starts listening for live snapshots. Here's an example of what that looks like:
struct Room: Model {
#DocumentID
var id: DocumentReference? = nil
var title: String
var description: String
static let collectionPath: String = "rooms"
}
struct MacOSView: View {
#Collection({ $0.order(by: "title") })
private var rooms: [Room]
var body: some View {
NavigationView {
List(rooms) { room in
NavigationLink(
destination: Lazy(ChatRoom(room))
) {
Text(room.title)
}
}
}
}
}
The closure inside #Collection is optional, but can be used to build a more precise query for the collection.
Now this works very nicely. The code is expressive and I get nice live-updating data. You can see that when the user would click on a room title, the navigation view would navigate to that chat room. The chatroom is a view which shows all the messages in that room. Here's a simplified view of that code:
struct ChatRoom: View {
#Collection(wait: true)
private var messages: [Message]
// I'm using (wait: true) to say "don't open a connection just yet,
// because I need to give you more information that I can't give you yet"
// And that's because I need to give #Collection a query
// based on the room id, which I can only do in the initializer.
init(_ room: Room) {
_messages = Collection { query in
query
.whereField("room", isEqualTo: room.id!)
.order(by: "created")
}
}
var body: some View {
List(messages) { message in
MessageBubble(message: message)
}
}
}
But what I've noticed, is that SwiftUI initializes a new messages-collection every single time the user interacts with the UI in any way. Like even when an input box is clicked to give it focus. This is a huge memory leak. I don't understand why this happens, but is there a way to tell SwiftUI to only initialize #Collection once, just like it does with #StateObject?
Use #StateObject when you want to define a new source-of-truth reference type owned by that View and tied to its life-cycle. The object will be created just before the body is run for the first time and is stored in a special place by SwiftUI. If the View struct is recreated (e.g. by a parent View's body recompute by a state change) then the previous object will be set on the property instead of a new object. If the View is no longer init during body updates then the object will be deinit.
When you pass a #StateObject into a child View then in that child View uses #ObservedObject to enable the body of the child view to be recomputed when the object changes, just the same way the body of the parent View will also be recomputed because it used an #StateObject. If you use #ObservedObject for an object that was not an #StateObject in a parent View then since no View is owning it then it will be lost every time the View is init during body updates. Also, any objects that you create in the View init, like your Collection, those are immediately lost too. These excessive heap allocations could cause a leak and slows down SwiftUI. Objects the View owns must be wrapped in #StateObject to avoid this.
Lastly, don't use #StateObject for view state and certainly don't try and implement a legacy "View Model" pattern with them. We have #State and #Binding for that. #StateObject is only for model objects (as in your domain types: Book, Person etc.), loaders or fetchers.
WWDC 2020 Data Essentials in SwiftUI explains all this very nicely.

How does this SwiftUI binding + state example manage to work without re-invoking body?

A coworker came up with the following SwiftUI example which looks like it works just as expected (you can enter some text and it gets mirrored below), but how it works is surprising to me!
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
TextField("Change the string", text: $text)
WrappedText(text: $text)
}
}
}
struct WrappedText: View {
#Binding var text: String
var body: some View {
Text(text)
}
}
My newbie mental model of SwiftUI led me to think that typing in the TextField would change the $text binding, which would in turn mutate the text #State var. This would then invalidate the ContentView, triggering a fresh invocation of body. But interestingly, that's not what happens! Setting a breakpoint in ContentView's body only gets hit once, while WrappedText's body gets run every time the binding changes. And yet, as far as I can tell, the text state really is changing.
So, what's going on here? Why doesn't SwiftUI re-invoke ContentView's body on every change to text?
On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text value (Binding is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText it is detected that Text with new text is not equal to one with old text, so body of WrappedText is called to re-render this part.
This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.
By default this mechanism works by View struct properties, but we can be involved in it by confirming our view to Eqatable protocol and marking it .equatable() modifier to give some more complicated logic for detecting if View should be (or not be) re-rendered.

Ambiguous reference with a Picker in Swift UI

I'm trying to make a Picker with SwiftUI. I've follow a tutorial but don't have the same result. There is Ambiguous reference on the self.category.count and self.category[$0]. After one entire day, I still don't know how to fix it ...
import SwiftUI
struct Picker : View {
var category = ["Aucun", "BF Glaive", "Baguette", "Negatron", "Larme", "Ceinture", "Arc", "Cotte", "Spatule"]
#State private var selectedCategory = 0
var body: some View {
VStack {
Picker(selection: $selectedCategory, label: Text("Item")) {
ForEach(0 ..< self.category.count) {
Text(self.category[$0])
.tag($0)
}
}
Text("Selected : \(category[selectedCategory])")
}
}
}
To resolve name conflicts between modules, you can either:
Rename your Picker to something else.
Use the qualified (full) name:
SwiftUI.Picker(selection: $selectedCategory, label: Text("Item")) {
The error message Ambiguous reference to member 'count’ is misleading. What you have is a naming conflict between SwiftUI.Picker and your Picker struct. Just change the name of your struct to something other than Picker. For example:
struct CategoryPicker : View {
// ...
}
Alternatively, you can resolve the naming conflict between the modules by providing the fully qualified name for SwiftUI.Picker (as Sulthan pointed out):
SwiftUI.Picker(selection: $selectedCategory, label: Text("Item")) {
// ...
}
However, I wouldn’t advise this option unless your intention is to replace SwiftUI.Picker everywhere in your app. Your code includes the category array and a Text view, so it's unlikely this is what you're after.
If the app eventually needs OtherPicker with a SwiftUI.Picker and the module name is omitted again, it’ll be even more confusing to track down the error—and you’ve already spent an “entire day” on it. So, best to avoid this possibility by not introducing the conflict at all :)