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>.
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 am finding that in multiple UIViewControllers I am setting the access of various reusable views so they can update their constraints accordingly. I'd like to create a SuperClass UIViewController called ChallengeFlowViewController so that it can take care of managing the calls that update the axis for views which support axis updates. Therein lies the challenge. How can I define a method or computed property so that it can be overridden and the subclasses can retur any number of different views so long as each of those views conform to HasViewModel and their ViewModel type conforms to HasAxis.
The following implementation has axisSettables(), however the way it is implemented, it requires all the views returned be the same view type. I want variance in view type to be allowed so long as all of them fit the requirements.
Another issue, in the viewWillLayoutSubView method, I'm getting the error: Generic parameter 'T' could not be inferred
class ChallengeFlowViewController: UIViewController {
/// override to ensure axises are set for views that adjust when rotated.
/// Styles the views if they conform to
func axisSettables<T: HasViewModel>() -> [T?] where T.ViewModel: HasAxis {
[]
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
for view in axisSettables() {
view?.setAxis()
}
}
}
protocol HasViewModel {
associatedtype ViewModel
var viewModel: ViewModel { get set }
init()
}
extension HasViewModel {
init(viewModel: ViewModel) {
self.init()
self.viewModel = viewModel
}
mutating func setAxis(
from newAxis: NSLayoutConstraint.Axis = UIApplication.orientationAxis
) where ViewModel: HasAxis {
guard viewModel.axis != newAxis else { return }
viewModel.axis = newAxis
}
}
extension UIApplication {
/// This is orientation as it relates to constraints.
enum ConstraintOrientation {
/// portrait and portraitUpsideDown equal portrait
case portrait
/// landscapeleft and landscaperight equal landscape
case landscape
}
/// Unfortunately `UIDevice.current.orientation` is unreliable in determining orientation.
/// if you `print(UIDevice.current.orientation)` when the simulator or phone launches
/// with landscape, you will find that the print value can sometimes print as portrait.
/// if statusBarOrientation is unknown or nil the fallback is portrait, as it would be the safer orientation
/// for people who have difficulty seeing, for example, if a view's landscape setting splits the views in half
/// horizontally, the same would look narrow on portrait, wheras the portrait setting is more likely to span
/// the width of the window.
static var constraintOrientation: ConstraintOrientation {
guard #available(iOS 13.0, *) else {
return UIDevice.current.orientation.constraintOrientation
}
return statusBarOrientation == .landscapeLeft || statusBarOrientation == .landscapeRight ? .landscape : .portrait
}
#available(iOS 13.0, *)
static var statusBarOrientation: UIInterfaceOrientation? {
shared.windows.first(where: \.isKeyWindow)?.windowScene?.interfaceOrientation
}
static var orientationAxis: NSLayoutConstraint.Axis {
constraintOrientation == .landscape ? .horizontal : .vertical
}
}
protocol HasAxis {
var axis: NSLayoutConstraint.Axis { get set }
}
extension HasAxis {
var horizontal: Bool { axis == .horizontal }
var vertical: Bool { axis == .vertical }
}
What prevents you from using a heterogenous array is the HasViewModel protocol, which has associated types, which forces you to use it as a generic argument for the axisSettables function, which leads to having to declare the return type as a homogenous array.
If you'll want to be able to use heterogenous arrays, then you'll need to use a "regular" protocol that is complementary to the HasViewModel one. For example:
protocol AxisSettable {
func setAxis()
}
class ChallengeFlowViewController: UIViewController {
/// override to ensure axises are set for views that adjust when rotated.
/// Styles the views if they conform to
func axisSettables() -> [AxisSettable] {
[]
}
This way you also keep the concerns separated, as axisSettables should not care if the returning views have or not a view model, it should only care if the views can update their axis. At the implementation level, you can keep the HasViewModel protocol, if that helps in other areas, and just add conformance to AxisSettable.
I wound up using an optional function to the HasViewModel protocol.
protocol HasViewModel {
optional mutating func setAxis(from newAxis: NSLayoutConstraint.Axis)
}
Now I can return views that conform to HasViewModel
/// Override me
var hasViewModelArray: [HasViewModel] {
}
Then the method is optionally available to me in the viewWillLayoutSubviews method.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
hasViewModelArray.forEach(\.setAxis?())
}
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 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.
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.