Initialise lazy variable in SwiftUI view - swift

I have a delegate in a view that depends on data inputted when initialising the view. The fileFormat should be passed into this delegate.
I tried using a lazy variable (to prevent uninitialised self errors), but as lazy variables mutate whenever first called, that wouldn't work:
struct EditorView: View {
let fileFormat: UTIType
#Binding var text: String
lazy var highlightDelegate = HighlightDelegate(format: fileFormat)
// something that uses `highlightDelegate`:
// Cannot use mutating getter on immutable value: 'self' is immutable
}
If I use a computed property, highlightDelegate would be initialised every time it gets called which isn't quite what I need - each view should initialise one of these structures.

You can initialise highlightDelegate in the init of your view, no need to make it lazy.
struct EditorView: View {
let fileFormat: UTIType
#Binding var text: String
let highlightDelegate: HighlightDelegate
init(fileFormat: UTIType, text: Binding<String>) {
self.fileFormat = fileFormat
self._text = text
self.highlightDelegate = HighlightDelegate(format: fileFormat)
}
...
}

Related

How to change a variable value inside a view (GeometryReader)

I have a geometry reader that tells me the height of a calendar. I need the value of it for another function that I declare outside of the view. How would I get the value outside of the view. I tried adding a variable which gets changed once the geometryreader is loaded, but as its a view I cannot change the value inside it.
My current attempt looked like this
Struct : Login: View {
#State public var overlayH = CGFloat()
func heightFromTop() {
var height = overlayH * (1/8)
}
var body: some View {
GeometryReader { scheduleOverlay in
overlayH = scheduleOverlay.size.height
}
}
}
But because you cannot change a variable inside the view I get a
Type '()' cannot conform to 'View'
error. Is there any work arounds to avoid this?

Unnecessary rendering issue if we use onChange or PreferenceKey modifier in SwiftUI

In this down code I am using Binding in a custom View called TextView, which will not use this Binding in it's body, It will just sitting there.
The things get weird when we use onChange or PreferenceKey in our project that makes changing behavior of View for unused Binding in body. So here is what happens if we don't use onChange or PreferenceKey in our project and we update that unused Binding in our project that doesn't make the view get rendered and even we don't need to defined and conform to Equitable function to help SwiftUI to understand that it is the same View. It will work even without defining Equitable function or conforming to Equitable protocol. But when we use onChange or PreferenceKey on that unused Binding and even if we use and define Equitable and Equitable function for the View it will render it any way! Even it is not necessary so I don't know how can I solve this issue!?
struct ContentView: View {
#State private var string: String = "Hello" {
didSet {
print(string)
}
}
var body: some View {
EquatableView(content: TextView(string: $string))
Button("update") { string += " updated!" }
//.onChange(of: string) { newValue in } // <<: Here!!!
}
}
struct TextView: View, Equatable {
#Binding var string: String
let value: String = "123"
var body: some View {
print("rendering TextView!", "- - - - - - ")
return Text(value)
}
static func == (lhs: TextView, rhs: TextView) -> Bool {
print("Equatable function used!")
return lhs.value == rhs.value
}
}
When you add .onChange(of: string) { ... } you are referencing string in body of ContentView, and if the value changes, body of ContentView will be executed - and thus body of TextView, too.
In your setup without .onChange you are not referencing the value of string in the body of ContentView and changes to the value do not cause ContentView to be redrawn. And with- or without EquatableView, body of TextView should not be called when clicking the button.
In your pseudo code, I think, you do not need EquatableView - because there's nothing which could be optimised, and SwiftUI's default behaviour works well.
So far, it works as expected.
Now your question boils down to: how can I avoid to redraw a child view if the super view changes? (read: how to not execute body of a child view, if body of the super view will be executed)
Well, I think, SwiftUI can do optimisations - but I don't believe it can generally omit re-drawing a child view, when the super view changes and has been redrawn.
And now, when I think about it, you may use "ContentView" as the argument to EquatableView. Since, ContenView (presumably) does not depend on string.

How/Where to assign CoreData entity's variable to structs's #State variable in SwiftUI?

I use #State variables (title, description) to communicate with the other view (UITextView). That view has a binding string variable (text).
I want to use specific CoreData entity (Item) to update my #State variables, so the text in UITextView is updated when view appears (so UITextView does not appear blank). As far as I understand, I should somehow assign that CoreData entity variable to my #State variable at first and, when I get updates from UITextView on my #State variables, I should save context to my CoreData entity. I use init() for initial assign and I don't get any errors until I get to live preview debugging. I get this error (as a debugger message):
Failed to call designated initializer on NSManagedObject class 'Item'
Also, the text is missing on the preview.
Maybe there's the other way to do what I want?
Here's my code:
Main view
struct DetailView: View {
#ObservedObject var item: Item = Item()
#State private var title = ""
#State private var description = ""
init (item: Note)
{
self.item = item
self.title = item.title!
...
TextView(text: self.$title...
...
TextView(text: self.$details...
...
TextView
struct TextView: UIViewRepresentable {
#Binding var text: String
...
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
...
P.S. when I directly pass (ignoring existence of my #State variables) in CoreData entity's property (e.g. self.item.title) as an argument for UITextView displaying text actually works but I don't know how to save the changes from UITextView to CoreData entity directly
There are a few ways of doing this.
There is the #FetchedRequest way for which there are many tutorials out there.
https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-fetchrequest-property-wrapper
With that method you would pass the users object that you are editing.
Also, you can use an FetchedResultsController.
https://www.youtube.com/watch?v=-U-4Zon6dbE
This way you can have an #ObservableObject with the CoreData objects
https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller
A ManagedObject or CoreData object already conforms to ObservableObject
https://developer.apple.com/documentation/coredata/nsmanagedobject
You don't need to put it in an #State you just need to pass the Fetched Object from CoreData.
Also, when the user is done with the object you need to save the managedObjectContext.

Immutable some view body variable/opaque return in View

In Swift 5.1 there are opaque types. I see that e.g. body is a required protocol variable. It's contract is defined as follows:
var body: Self.Body { get }
It means we should be able to mark body as immutable (no set). How must this be done? Is it possible for opaque variable types to be immutable? I tried:
import SwiftUI
struct ContentView : View {
init() {
body = AnotherView(body: Text(""))
}
let body: some View
}
struct AnotherView: View {
var body: Text
}
But I get the error that AnotherView must be casted to some View. After doing that, I get the error:
'some' types are only implemented for the declared type of properties
and subscripts and the return type of functions
Am I able to conform to View with immutable body variables which are of type some View (not marking it explicit as AnotherView)? AnotherView is some View, I don't understand why I can't just assign the instance of AnotherView to body. I want to remain flexible and not expose the actual implementation type of body outside the struct, but I want to initialize it directly inside the initializer (because I am passing in values inside the initializer, making more properties and use them in the body property is verbose).
Because there is no setter, any body implementation that is a value type will be immutable. The var just means that body is lazily evaluated, not that it is mutable. You could declare let body, but, as you point out, this exposes the underlying View's implementation:
public struct StaticTextView : View {
public let body: Text
public init(string: String) {
self.body = Text(string)
}
}
One way you could fix this would be to have body just return an internal private value, like so:
public struct StaticTextView : View {
private let textView: Text
public var body: some View { textView }
public init(string: String) {
self.textView = Text(string)
}
}
However, you should bear in mind that body is designed to be run dynamically whenever any of the bound state changes, and if you want to assign your view to a constant, nothing in that view hierarchy could be bound to any dynamic state. For example, this would not be possible:
struct DynamicStepperView : View {
#State var stepperValue = 1
var body: some View {
Stepper(value: $stepperValue, in: 1...11, label: { Text("Current Value: \(stepperValue)") })
}
}
If your primary concern is preventing the leaking of implementation details of your view hierarchy, note that the opaque return type of some View is indeed opaque to any clients of the code, and they will not be able to see any of the details of the underlying implementation other than that it is something that conforms to the View protocol.

Why/when are uninitialized non-optional values allowed in Swift for EnvironmentObjects?

In Swift, this crashes at runtime:
class EmptyData: BindableObject {
let didChange = PassthroughSubject<EmptyData, Never>()
}
struct RandomView : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
Text("Hello World!")
}
}
and in the SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let window = UIWindow(frame: UIScreen.main.bounds)
// The emptyData variables are not initialized as seen below
window.rootViewController = UIHostingController(rootView: RandomView())
self.window = window
window.makeKeyAndVisible()
}
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Fixing the problem isn't that hard, but rather strange:
window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()))
So what's happening here? I pass EmptyData() and SwiftUI decides that both emptyData and emptyData2 should be initialized with the same object reference? I can pass also other environmentobjects that do not even exists as variables in the RandomView instance:
window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()).environmentObject(SomeData()))
And SwiftUI just happily run, although SomeData() isn't used anywhere in the instance of RandomView() and should trigger a compile time error in my opinion.
Why are uninitialized values permitted at compile time without initializing them when initializing the object and why are we free to pass environment instances without doing anything with them? Looks a bit like Javascript to me, I loved the strong static safe typing in Swift... I don't see right away why the member-wise initializer just generates an initializer which takes the environment variables as it's parameter.
The EnvironmentObject property delegate has an init() method taking no parameters, and that provides an implicit initialization for the wrapped properties
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
(this is explained in the Modern Swift API Design video roughly at 28:10). So that is why these (non-optional) properties do not need an (explicit) initial value.
The documentation also states that EnvironmentObject is (emphasis added)
... a dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view whenever the bindable object changes.
You must set a model object on an ancestor view by calling its environmentObject(_:) method.
So this is how I understand it:
If a matching bindable object (in your case: an instance of EmptyData) is found in the environment of the current view or one of its ancestors then the properties are initialized to this object.
If no matching bindable object if found in an ancestor view then the program terminates with a runtime error.
Environment objects can be used in all, some, or none of the views in the view hierarchy. (See Data Flow Through SwiftUI at 29:20.) Therefore it is not an error to provide an environment object (in your case: an instance of SomeData) which is not used in RandomView.
What is #EnvironmentObject?
A linked View property that reads a BindableObject supplied by an
ancestor
So, the environment prop can be supplied to children from the ancestor, not necessarily it should come from its immediate parent.
With that, take a look at the below snippet, since RandomViewGrandParent injects the required Env objects into the environment, RandomViewParent doesn't have to do anything if the children of RandomViewParent needs same Env obj. RandomViewParent can just initiate view without passing the env obj again.
class EmptyData: BindableObject {
let didChange = PassthroughSubject<EmptyData, Never>()
}
struct RandomViewGrandParent : View {
var body: some View {
RandomViewParent().environmentObject(EmptyData())
}
}
struct RandomViewParent : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
RandomView()
}
}
struct RandomView : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
Text("Hello World!")
}
}
And to ans your another question -
I pass EmptyData() and SwiftUI decides that both emptyData and
emptyData2 should be initialized with the same object reference?
That's because EnvironmentObject conforms to BindableObject and BindableObject's didChange is a Publisher, so I believe it thinks both emptyData and emptyData2 wants to subscribe to the same events/values hence uses the same ref for both.