SwiftUI - PageView - Pass in different Views - swift

I successfully implemented PageView within SwiftUI via thread:
How to implement PageView in SwiftUI?
Passing in multiple Views via an Array works like a charm, as long as all views are of the same struct.
PageView([TestView(), TestView()]).
However, I'd like to pass in different views.
PageView([TestView(), AnotherView(), DifferentView()]).
All views are of SwiftUI type:
struct NAME : View { code }
When I try to add different structs to an array I get the following error message:
var pageViewViewArray = [TestView(), AnotherView(), DifferentView()]
Heterogeneous collection literal could only be inferred to '[Any]';
add explicit type annotation if this is intentional.
Insert' as [Any]
By casting it to:
var pageViewViewArray = [TestView(), AnotherView(), DifferentView()] as! [Any]
PageView(pageViewViewArray)
PageView will say:
Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols
I'll greatly appreciate any ideas.

Try using type erasure by casting every view to AnyView:
var pageViewViewArray: [AnyView] = [AnyView(TestView()), AnyView(AnotherView()), AnyView(DifferentView())]
Documentation here, and an example of using it here.

There is a more efficient way to do it, without type erasure. You should create a common view in which you inject an enum value based on which you then return a desired view. Take a look at an example below:
/// The first type of a view that accepts and shows a number
struct ViewA: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
/// The second type of a view that accepts and shows a string
struct ViewB: View {
let string: String
var body: some View {
Text(string)
}
}
/// An enum type used for dependency injection
enum ViewType {
case viewA(number: Int)
case viewB(string: String)
}
/// A common view which internally handles different kind of views based on injected enum value
struct CommonView: View {
let viewType: ViewType
var body: some View {
switch viewType {
case .viewA(let number):
ViewA(number: number)
case .viewB(let string):
ViewB(string: string)
}
}
}
// Views used for page view controller, that are now of same type:
var pageViewViewArray = [
CommonView(viewType: .viewA(number: 1)),
CommonView(viewType: .viewB(string: "Hello, World!"))
]

Related

A struct mutating properties or ObservableObject Published properties to drive data changes

I would like help to further understand the implications of using the following 2 methods for driving data between multiple views.
My situation:
A parent view initialises multiple child views with data passed in.
This data is a big object.
Each view takes a different slice of the data.
Each view can manipulate the initial data (filtering, ordering etc)
Using an observableObeject to store this data and multiple published properties for each view :
can be passed in as an environment object that can be accessed by any view using #EnvironmentObject.
You can create a Binding to the published properties and change them.
Execute a method on the ObservableObject class and manipulate a property value which gets published using objectWillChange.send() inside the method.
I have achieved the desired listed above by using a struct with mutating methods. Once these properties are changed in the struct, the views which bind to these properties causes a re-render.
My struct does not do any async work. It sets initial values. Its properties are modified upon user action like clicking filter buttons.
Example
struct MyStruct {
var prop1 = "hello"
var prop2: [String] = []
init(prop2: [String]) {
self.prop2 = prop2
}
mutating func changeProp2(multiplier: Int) {
let computation = ...
prop2 = computation //<----- This mutates prop2 and so my view Binded to this value gets re-renderd.
}
}
struct ParentView: View {
var initValue: [String] // <- passed in from ContentView
#State private var myStruct: MyStruct
init(initValue: [String]) {
self.myStruct = MyStruct(prop2: initValue)
}
var body: some View {
VStack {
SiblingOne(myStruct: $myStruct)
SiblingTwo(myStruct: $myStruct)
}
}
}
struct SiblingOne: View {
#Binding var myStruct: MyStruct
var body: some View {
HStack{
Button {
myStruct.changeProp2(multiplier: 10)
} label: {
Text("Mutate Prop 2")
}
}
}
}
struct SiblingTwo: View {
#Binding var myStruct: MyStruct
var body: some View {
ForEach(Array(myStruct.prop2.enumerated()), id: \.offset) { idx, val in
Text(val)
}
}
}
Question:
What use cases are there for using an ObservableObject than using a struct that mutates its own properties?
There are overlap use cases however I wish to understand the differences where:
Some situation A favours ObservableObject
Some situation B favours struct mutating properties
Before I begin, when you say "these properties causes a re-render" nothing is actually re-rendered all that happens is all the body that depend on lets and #State vars that have changed are invoked and SwiftUI builds a tree of these values. This is super fast because its just creating values on the memory stack. It diffs this value tree with the previous and the differences are used to create/update/remove UIView objects on screen. The actual rendering is another level below that. So we refer to this as invalidation rather than render. It's good practice to "tighten" the invalidation for better performance, i.e. only declare lets/vars in that View that are actually used in the body to make it shorter. That being said no one has ever compared the performance between one large body and many small ones so the real gains are an unknown at the moment. Since these trees of values are created and thrown away it is important to only init value types and not any objects, e.g. don't init any NSNumberFormatter or NSPredicate objects as a View struct's let because they are instantly lost which is essentially a memory leak! Objects need to be in property wrappers so they are only init once.
In both of your example situations its best to prefer value types, i.e. structs to hold the data. If there is just simple mutating logic then use #State var struct with mutating funcs and pass it into subviews as a let if you need read access or #Binding var struct if you need write access.
If you need to persist or sync the data then that is when you would benefit from a reference type, i.e. an ObservableObject. Since objects are created on the memory heap these are more expensive to create so we should limit their use. If you would like the object's life cycle to be tied to something on screen then use #StateObject. We typically used one of these to download data but that is no longer needed now that we have .task which has the added benefit it will cancel the download automatically when the view dissapears, which no one remembered to do with #StateObject. However, if it is the model data that will never be deinit, e.g. the model structs will be loaded from disk and saved (asynchronously), then it's best to use a singleton object, and pass it in to the View hierarchy as an environment object, e.g. .environmentObject(Store.shared), then for previews you can use a model that is init with sample data rather that loaded from disk, e.g. .environmentObject(Store.preview). The advantage here is that the object can be passed into Views deep in the hierarchy without passing them all down as let object (not #ObservedObject because we wouldn't want body invovked on these intermediary Views that don't use the object).
The other important thing is your item struct should usually conform to Identifiable so you can use it in a ForEach View. I noticed in your code you used ForEach like a for loop on array indices, that's a mistake and will cause a crash. It's a View that you need to supply with Indentifiable data so it can track changes, i.e. moves, insertions, deletions. That is simply not possible with array indices, because if the item moves from 0 to 1 it still appears as 0.
Here are some examples of all that:
struct UserItem: Identifiable {
var username: String
var id: String {
username
}
}
class Store: ObservableObject {
static var shared = Store()
static var preview = Store(preview: true)
#Published var users: [UserItem] = []
init(preview: Bool = false) {
if (preview) {
users = loadSampleUsers()
}
else {
users = loadUsersFromDisk()
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Store.shared)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach($store.users) { $user in
UserView(user: $user)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Store.preview)
}
}
struct UserView: View {
#Binding var user: UserItem
var body: some View {
TextField("Username", text: $user.username)
}
}

How to use any Object and pass it into a Generic SwiftUI View?

Context
I have a Protocol called Component and a Computed Variable, that returns a specific Object conforming to Component as any Component.
I also have a Generic SwiftUI View accepting a Component.
Compiler Error: Type 'any Component' cannot conform to 'Component'
Code
protocol Component: ObservableObject { var name: String }
struct ComponentView<C: Component>: View {
#ObservedObject var component: C
var body: some View { Text(c.name) }
}
struct RootView: View {
var body: some View {
ComponentView(component: component) // Compiler Error
}
private var component: any Component { // Returns any Component }
}
Question
I understand that any Component and Component are two different Types. However, I am looking for a possibility to use the given Component in the ComponentView. How can I achieve this goal of using the returned Component inside ComponentView?
The var component: any Component declaration means that the property can virtually hold any value that conforms to the protocol, meaning the exact type of the value stored there is to be determined at runtime.
On the other hand, ComponentView<C: Component> is a compile-time construct, and the compiler needs to know which type will fill in the blanks, something that is not possible due to the any construct.
You'll have to either
propagate any downstream - from RootView to ComponentView, and remove the generic on ComponentView, or
propagate the generic requirement upstream - from ComponentView to RootView, and make RootView generic.

Using protocol in SwiftUI for providing "some View" / Generics?

I'm trying to get my head something in SwiftUI. I want to build a SwiftUI view and have something you could call a ViewProvider as a #State var. something like this:
protocol ViewProvider {
associatedtype ViewOne = View
associatedtype ViewTwo = View
#ViewBuilder var viewOne: ViewOne { get }
#ViewBuilder var viewTwo: ViewTwo { get }
}
struct ContentView: View {
#State private var parent: ViewProvider?
var body: some View {
VStack {
HStack {
Button(action: { parent = Father() }, label: { Text("Father") })
Button(action: { parent = Mother() }, label: { Text("Mother") })
}
if let p = parent {
p.viewOne
p.viewTwo
}
}
}
}
class Father: ViewProvider {
#ViewBuilder var viewOne: some View {
Text("Father One!")
}
#ViewBuilder var viewTwo: some View {
Text("Father Two!")
}
}
class Mother: ViewProvider {
#ViewBuilder var viewOne: some View {
Text("Mother One!")
}
#ViewBuilder var viewTwo: some View {
Text("Mother Two!")
}
}
This produces 2 different compiler errors.
#State private var parent: ViewProvider?
// Protocol 'ViewProvider' can only be used as a generic constraint because it has Self or associated type requirements
and
p.viewOne
p.viewTwo
// 2x Member 'viewOne' cannot be used on value of protocol type 'ViewProvider'; use a generic constraint instead
I have a vague idea of what I'm doing wrong, but no idea on how to solve it :)
What syntax should I use to get something like this to work?
Assuming you're on Swift 5.6 or lower, the problem is that you can only use protocols with associated types for conformance, ie you can't use them as types to pass around. The reasoning is that their associated types will be different for different conformers.
Say you have the following:
protocol P {
associatedtype T
var prop: T
}
class MyClass: P {
var prop: Int
}
class MyOtherClass: P {
var prop: String
}
What would the result of the following be?
let arr: [P] = [MyClass(), MyOtherClass()]
let myMappedArr = arr.map { $0.prop }
prop is of a different type for each conformer.
In Swift 5.7, however, you actually can pass around protocols of this sort. In later versions of Swift, you will have to use the keyword any to pass these protocols around as types.
See the proposal for unlocked existentials to learn more about it.
Lastly to address opaque types here:
Since you can't pass around protocols with associated types, you can't have something like
#State var myState: ViewProvider or even #State var myState: some ViewProvider, because your state variable is assigned, and you can't assign something of an opaque type.
In SwiftUI's View, this works because the view property is computed, and thus the type can be inferred
// type is inferred to be (something like) Group<Text>
var body: some View {
Group {
Text("something")
}
}
whereas here, you can't find a suitable type to assign to a property whose type is opaque
#State var myState: some ViewProvider
...
// You don't know myState's type, so you can't assign anything to it
myState = ... // error - you won't be able to find a matching type to assign to this property
To wit, the line #State private var parent: ViewProvider? in your code simply won't compile in Swift 5.6 or lower, because you're not allowed to use your ViewProvider protocol as a type for anything other than conformance or as an opaque return type when used in functions or computed properties.
Sorry for all the edits. Wanted to provide a couple of potential solutions:
One way is to simply make your ContentView generic over the type of its ViewProvider
struct ContentView<ViewProviderType: ViewProvider> {
#State private var parent: ViewProviderType?
...
}
The other would be to simply remove the associatedtype from your protocol and just erase the view type you're trying to return:
protocol ViewProvider {
var viewOne: AnyView { get }
var viewTwo: AnyView { get }
}
If you're working with Swift 5.7, you may be able to use your type-constrained protocols as property types, or you can also use primary associated types, wherein you could declare properties of type ViewProvider<MyView> (though that doesn't necessarily solve your problem).
Generics or type erasure over ViewProvider's view types are probably the best candidates for what you're trying to do, even in a Swift 5.7 world.

How to bind an array and List if the array is a member of ObservableObject?

I want to create MyViewModel which gets data from network and then updates the arrray of results. MyView should subscribe to the $model.results and show List filled with the results.
Unfortunately I get an error about "Type of expression is ambiguous without more context".
How to properly use ForEach for this case?
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var results: [String] = []
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.results = ["Hello", "World", "!!!"]
}
}
}
struct MyView: View {
#ObservedObject var model: MyViewModel
var body: some View {
VStack {
List {
ForEach($model.results) { text in
Text(text)
// ^--- Type of expression is ambiguous without more context
}
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(model: MyViewModel())
}
}
P.S. If I replace the model with #State var results: [String] all works fine, but I need have separate class MyViewModel: ObservableObject for my purposes
The fix
Change your ForEach block to
ForEach(model.results, id: \.self) { text in
Text(text)
}
Explanation
SwiftUI's error messages aren't doing you any favors here. The real error message (which you will see if you change Text(text) to Text(text as String) and remove the $ before model.results), is "Generic parameter 'ID' could not be inferred".
In other words, to use ForEach, the elements that you are iterating over need to be uniquely identified in one of two ways.
If the element is a struct or class, you can make it conform to the Identifiable protocol by adding a property var id: Hashable. You don't need the id parameter in this case.
The other option is to specifically tell ForEach what to use as a unique identifier using the id parameter. Update: It is up to you to guarentee that your collection does not have duplicate elements. If two elements have the same ID, any change made to one view (like an offset) will happen to both views.
In this case, we chose option 2 and told ForEach to use the String element itself as the identifier (\.self). We can do this since String conforms to the Hashable protocol.
What about the $?
Most views in SwiftUI only take your app's state and lay out their appearance based on it. In this example, the Text views simply take the information stored in the model and display it. But some views need to be able to reach back and modify your app's state in response to the user:
A Toggle needs to update a Bool value in response to a switch
A Slider needs to update a Double value in response to a slide
A TextField needs to update a String value in response to typing
The way we identify that there should be this two-way communication between app state and a view is by using a Binding<SomeType>. So a Toggle requires you to pass it a Binding<Bool>, a Slider requires a Binding<Double>, and a TextField requires a Binding<String>.
This is where the #State property wrapper (or #Published inside of an #ObservedObject) come in. That property wrapper "wraps" the value it contains in a Binding (along with some other stuff to guarantee SwiftUI knows to update the views when the value changes). If we need to get the value, we can simply refer to myVariable, but if we need the binding, we can use the shorthand $myVariable.
So, in this case, your original code contained ForEach($model.results). In other words, you were telling the compiler, "Iterate over this Binding<[String]>", but Binding is not a collection you can iterate over. Removing the $ says, "Iterate over this [String]," and Array is a collection you can iterate over.

Why SwiftUI's View Protocol use PAT?

View protocol is defined like this:
public protocol View : _View {
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
associatedtype Body : View
/// Declares the content and behavior of this view.
var body: Self.Body { get }
}
so View is now a PATed protocol, which can't be used as a return type directly, though swift 5.1's opaque return type can handle this, but why declare a associatedtype Body : View, not var body: View { get } directly?
Because if it is just a var body: Self.Body { get } - your entity, that implements View protocol, will not know the type of the body.
struct MyView: View {
var body: MyAnotherView {
//implementation...
}
}
This code will not compile and you would have to write this:
struct MyView: View {
var body: View {
//implementation...
}
}
And I think behind the scenes SwiftUI has to know the exact type of a View, not just a protocol
Prior to SwiftUI Swift doesn’t allow us to use protocols with associated types as return types but We can use “regular” protocols.
Compiler let you restrict by showing below error:
“Protocol can only be used as a generic constraint because it has Self or associated type requirements.”
What does it mean?
Compiler cannot infer the associated type from this definition and the return type would be incomplete.
Whenever we call the that function it always return different concrete type instead if same concrete type.
Compiler will not let you to perform the swapping, equal, compare operation on this concrete type. Even they adopt the same Protocol ( i.e it's PAT).
Because concret type may have different associated type that they fulfilled or use.
To avoid the different concrete type as return type at each call we use some keyword as opaque return type.
Opaque return type:
It's reverse of generic type.
It always return same concrete type.you and compiler know it.
If we use an opaque result type instead, we enforce that the function will always return the same concrete type.
Inside function while performing the operation we know the generic type.