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?
Related
I would like to create a ViewModifier where the output is conditional on the type of content it is modifying.
The best test of the concept I've managed (using Text and TextField as example View types) is as follows:
struct CustomModifier<T: View>: ViewModifier {
#ViewBuilder func body(content: Content) -> some View {
if content is Text.Type {
content.background(Color.red)
} else {
if content is TextField<T>.Type {
content.background(Color.blue)
}
}
content
}
}
The problem with the above modifier is that you need to explicitly provide the generic term when you use the modifier so seems incorrect (and, to my nose, a code smell) as you then need to define a generic on the parent View and then a generic on its parent, etc, etc..
e.g.
struct ContentView<T: View>: View {
var body: some View {
VStack {
Text("Hello world")
.modifier(CustomModifier<Text>())
TextField("Textfield", text: .constant(""))
.modifier(CustomModifier<TextField<T>>())
}
}
}
I managed to get around this problem (with some guidance from Cristik) using this extension on View:
extension View {
func customModifier() -> some View where Self:View {
modifier(CustomModifier<Self>())
}
}
The modifier was tested using the following using iPadOS Playgrounds:
struct ContentView: View {
var body: some View {
Form {
Text("Hello")
.customModifier()
TextField("Text", text: .constant(""))
.customModifier()
}
}
}
This compiles and runs but the output is not what I expected. The Text and TextField views should have different backgrounds (red and blue respectively) but they are displayed unchanged. Overriding the View type checking in the modifier (hard coding the type check to 'true') results in a background colour change so the modifier is being applied; it's the type check that's failing.
I dragged the code into Xcode to try and get a better idea of why this was happening and got an immediate compiler warning advising that the type check would always fail (the modifier name in the screenshot is different - please disregard):
Xcode compiler errors:
This explains why the code does not perform as intended but I'm unable to determine whether I have made a mistake or if there is (in real terms) no way of checking the concrete type of a View sent to a ViewModifier. As best I can tell, the content parameter sent to a ViewModifier does seem to be type erased (based on the methods accessible in Xcode) but there does seem a way of obtaining type information in this context because certain modifiers (e.g. .focused()) only operate on certain types of View (specifically, interactive text controls) and ignore others. This could of course be a private API that we can't access (yet...?)
Any guidance / explanation?
You're right, there are some code smells in that implementation, starting with the fact that you need to write type checks to accomplish the goal. Whenever you start writing is or as? along with concrete types, you should think about abstracting to a protocol.
In your case, you need an abstraction to give you the background color, so a simple protocol like:
protocol CustomModifiable: View {
var customProp: Color { get }
}
extension Text: CustomModifiable {
var customProp: Color { .red }
}
extension TextField: CustomModifiable {
var customProp: Color { .blue }
}
, should be the way to go, and the modifier should be simplifiable along the lines of:
struct CustomModifier: ViewModifier {
#ViewBuilder func body(content: Content) -> some View {
if let customModifiable = content as? CustomModifiable {
content.background(customModifiable.customProp)
} else {
content
}
}
}
The problem is that this idiomatic approach doesn't work with SwiftUI modifiers, as the content received as an argument to the body() function is some internal type of SwiftUI that wraps the original view. This means that you can't (easily) access the actual view the modifier is applied to.
And this is why the is checks always failed, as the compiler correctly said.
Not all is lost, however, as we can work around this limitation via static properties and generics.
protocol CustomModifiable: View {
static var customProp: Color { get }
}
extension Text: CustomModifiable {
static var customProp: Color { .red }
}
extension TextField: CustomModifiable {
static var customProp: Color { .blue }
}
struct CustomModifier<T: CustomModifiable>: ViewModifier {
#ViewBuilder func body(content: Content) -> some View {
content.background(T.customProp)
}
}
extension View {
func customModifier() -> some View where Self: CustomModifiable {
modifier(CustomModifier<Self>())
}
}
The above implementation comes with a compile time benefit, as only Text and TextField are allowed to be modified with the custom modifier. If the developer tries to apply the modifier on a non-accepted type of view, they-ll get a nice Instance method 'customModifier()' requires that 'MyView' conform to 'CustomModifiable', which IMO is better than deceiving about the behaviour of the modifier (i.e. does nothing of some views).
And if you need to support more views in the future, simply add extensions that conform to your protocol.
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)
}
...
}
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.
I'd like to implement a navigator/router for an architecture implemented with SwiftUI and Combine. In a few words the View will share viewModel with Router. When the View triggers a change on the viewModel the Router should navigate to a new sheet.
This is a version of my code where I'm directly passing the viewModel from View to Router. Is there anything wrong? My biggest doubt is that since I'm using #ObservedObject on both the Router and the View, two different instances of the viewModel are created.
VIEW MODEL
class BootViewModel:ObservableObject{
#Published var presentSignIn = false
}
VIEW
struct BootView: View {
#ObservedObject var viewModel:BootViewModel
var navigator:BootNavigator<BootView>? = nil
init(viewModel:BootViewModel) {
self.viewModel = viewModel
self.navigator = BootNavigator(view: self, viewModel: viewModel)
self.navigator.setSubscriptions()
}
var body: some View {
VStack{
Text("Hello")
Button("Button"){
self.viewModel.presentSignIn.toggle()
}
}
}
}
NAVIGATOR
class BootNavigator<T:View>{
var view:T? = nil
#ObservedObject var viewModel:BootViewModel
init(view:T, viewModel:BootViewModel) {
self.view = view
self.viewModel = viewModel
}
func setSubscriptions(){
subscribe(onSigninPressed: $viewModel.presentSignIn)
}
func subscribe(onSigninPressed : Binding<Bool>){
_ = view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
}
}
}
Why the SignInView is never presented?
Without taking into account the fact that using a router with swiftUI is not needed in general(I'm mostly doing an exercise)... is there anything wrong with this implementation?
This
view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
MUST be somewhere in body (directly or via computed property or func) but inside body's ViewBuilder
Some notes I have to point out here:
ValueType
There is a difference between an UIView and a SwiftUI View. All SwiftUI Views are value type! So they get copied when you pass them around. Be aware of that.
Single instance
If you want a single instance like a regular navigator for your entire app, you can use singleton pattern. But there is a better approach in SwiftUI universe called #Environment objects. You can take advantage of that.
Trigger a view refresh
To refresh the view (including presenting something), you must code inside the var body. But it can be directly written on indirectly through a function or etc.
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.