Implementing a custom ViewModifier where output is conditional on concrete View type (SwiftUI) - swift

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.

Related

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 can I use inout instead of Binding in SwiftUI?

I like to know if I could make my view function bind itself via inout instead of Binding, is it possible in right/better syntax for us? right know I am getting compile time error of:
Modifying state during view update, this will cause undefined behavior.
which is understandable for me, I thought maybe I am using-wrong syntax for this work.
PS: I completely know about Binding and it use case, this question try find answer if we could do it with inout as well.
func TextView(inPutValue: inout Bool) -> some View {
return Text("Hello")
.onTapGesture { inPutValue.toggle() }
}
use case:
struct ContentView: View {
#State private var isOn: Bool = true
var body: some View {
TextView(inPutValue: &isOn)
}
}
update:
import SwiftUI
struct ContentView: View {
#State private var value: Int = Int() { didSet { print(value.description) } }
var body: some View {
Button("update State via inout") { inoutFunction(incomingValue: &value) }
}
}
func inoutFunction(incomingValue: inout Int) { incomingValue += 1 }
The reason why you can't use inout here is because inout has a copy-in-copy-out behaviour. The value is passed into the function as a copy, and as soon as the function returns, the modified copy is copied back. You should not think of it as pass-by-reference, thought it can be implemented this way as an optimisation.
Now knowing that, it'd make a lot of sense for the Swift compiler to forbid you from using an inout parameter in an escaping closure, such as using inPutValue in onTapGesture. After all, the modified inPutValue is only copied back when TextView returns, not when someone taps it, so whatever modifications you do to it in onTapGesture is not visible to the caller of TextView at all. See also this answer.
So the value is copied back to the caller as soon as TextView returns. As the error message says, modifying a #State directly when computing body (i.e. "during view update") is not allowed.
Now let's look at the case of Button. Note that this time, the call to inoutFunction(incomingValue: &value) happens inside the button's click handler, rather than in body - meaning value will be written to when the button is pressed. This is allowed.
You can make your TextView to be of a similar form to Button's initialiser by adding a closure argument:
func TextView(update: #escaping () -> Void) -> some View {
return Text("Hello")
.onTapGesture(perform: update)
}
Note that there is nothing inout in this at all, just like there is nothing inout in Button.init.
You can then write your function with an inout parameter:
func inoutFunction(_ b: inout Bool) {
b.toggle()
}
and use it:
#State private var isOn = true
var body: some View {
TextView { inoutFunction(incomingValue: &isOn) }
}
Notice that you don't need inout at all.
TextView { isOn.toggle() }

Using Custom UIKit Classes in SwiftUI

I have created my own custom classes for UIKit objects. now i want to use same classes in SwiftUI, How can i achieve that and how much effort it will take.
Also if i want i will need to write same classes in swift UI.
example, I have custom UILable subclass WMLabel.
class WMLabel: UILabel {
var myproperty: String = "" {
didSet {
self.text = myproperty
}
}
}
so how can i use WMLabel in swiftUI?
I have tried ObserverableObject and UIViewRepresentable, but not able to access the properties.
You can definitely use your UIKit classes. To get basic access to the properties, you'll want to be looking at makeUIView, which occurs when the view is first created and updateUIVew.
Using your example code:
class WMLabel: UILabel {
var myproperty: String = "" {
didSet {
self.text = myproperty
}
}
}
struct WMLabelRepresented: UIViewRepresentable {
var text : String
func makeUIView(context: Context) -> WMLabel {
return WMLabel()
}
func updateUIView(_ uiView: WMLabel, context: Context) {
uiView.myproperty = text
}
}
struct ContentView : View {
var body: some View {
WMLabelRepresented(text: "My text")
}
}
If there are things that can't be expressed declaratively, you'll want to look into coordinators and as you mentioned, possible an ObservableObject to communicate data imperatively to your view, but often you can find ways to express most things declaratively.
If you want an example of more complex imperative communication, here's a couple of links to another answers of mine:
https://stackoverflow.com/a/65926143/560942
https://stackoverflow.com/a/66845387/560942
Converting all of your custom classes and interfacing with them is going to be one heck of a chore if you have a few of them. You would end up using something called UIViewRepresentable which requires quite a few things, the most annoying of which called a coordinator. You'd almost be better off rewriting your classes into a SwiftUI version. Here's Apple's documentation on interfacing SwiftUI with UIKit: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
Here's an example of conversion of that UILabel into SwiftUI, with accessible properties.
Example Conversion
struct WMLabel: View {
var myProperty: String
var body: some View {
Text(myProperty)
}
}
Example Usage
struct Example: View {
var body: some View {
WMLabel(myProperty: "Hello World!")
}
}
As you can see, there is little code involved in converting something to SwiftUI, if you start getting involved in UIViewRepresentable you have to start playing with coordinators and a bunch of other interfacing methods just to make it work. Sometimes it's required, but in most cases I'd try and avoid it.

SwiftUI Reusable View via Protocol Inheritance

The Goal
The goal I have is to make a reusable view protocol that inherits the SwiftUI View protocol and provides some default functionality, for loading showing different views based on the status of the views content.
This way, I don't have to rewrite identical code for every view I create and makes code cleaner.
The Problem
I made a "DelayedContentView" protocol that displays two different view bodies based on whether the view's content has loaded. The problem arises when I try to implement it. The "loadedBody" and "unloadedBody" properties can't be of type "some View" even though they are the same type as the SwiftUI View's Body associatedType.
Background
I have a various views in my app that fetch data remotely. Each view shows two view bodies: one for when the content is being fetched and the other for when the fetch is finished.
Apple's SwiftUI View Protocol
public protocol View {
associatedtype Body : View
var body: Self.Body { get }
}
My View Protocol
protocol DelayedContentView:View {
func contentLoaded() -> Bool
var loadedBody: Self.Body { get }
var unloadedBody: Self.Body { get }
}
extension DelayedContentView {
//Default implementation that I won't have to rewrite for each view.
var body: some View {
if contentLoaded() {
return self.loadedBody
}else{
return self.unloadedBody
}
}
}
Implementation
struct ExampleView:DelayedContentView {
func contentLoaded() -> Bool {
//Ask viewModel if content is loaded.
return false
}
var loadedBody: some View {
Text("Content is loaded.")
}
var unloadedBody: some View {
Text("Fetching content...")
}
}
Compile Error: Type 'ExampleView' does not conform to protocol 'DelayedContentView'
I thought protocol inheritance was the right tool for a case like this, but maybe I'm mistaken?
The protocol DelayedContentView has to add two more associated types to represent two additional views types: one for a "loaded" view and one - for "unloaded". It cannot be Self.Body because it clearly will be violated since you need a conditional view in the body.
protocol DelayedContentView: View {
associatedtype LoadedBody: View
associatedtype UnloadedBody: View
func contentLoaded() -> Bool
var loadedBody: LoadedBody { get }
var unloadedBody: UnloadedBody { get }
}
Now that you have two of those generic types, Self.Body will be a composite type of the two:
extension DelayedContentView {
#ViewBuilder // needed to create a _ConditionalContent type from if/else
var body: some View {
if contentLoaded() {
self.loadedBody
} else {
self.unloadedBody
}
}
}
Now, your specific View will declare what LoadedView and UnloadedView types are. For example, in your ExampleView, they would both be Text, and Body would be _ConditionalContent<Text,Text>.

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.