SwiftUI factory method issue in subclasses - swift

I develop a SwiftUI app in which I created a model entities inheritance hierarchy. The entities have a view property that returns a View instance. The intention is to build a List from an array of entities.
The model code:
protocol EntityProtocol: Identifiable {
associatedtype Content: View
#ViewBuilder var view: Content { get }
}
extension EntityProtocol {
#ViewBuilder var view: some View {
EmptyView()
}
}
class RootEntity: EntityProtocol {
let id = UUID()
}
class EntityA: RootEntity {
}
extension EntityA {
#ViewBuilder var view: some View {
Text("This is from EntityA")
}
}
The intention is when I have multiple subclasses of RootEntity each of the subclasses will return its own view.
Then I have my ContentView:
struct ContentView: View {
var entities: [RootEntity] = [EntityA()]
var body: some View {
List {
ForEach(entities) { entity in
entity.view
}
}
}
}
I have one item in the entities array but I get an empty view as a result, the view property is not called inside ForEach.
However, when I type cast to EntityAn inside ForEach it works as expected and shows "This is from EntityA" label:
struct ContentView: View {
var entities: [RootEntity] = [EntityA()]
var body: some View {
List {
ForEach(entities) { entity in
if let entityA = entity as? EntityA {
entityA.view
}
}
}
}
}
I'm using Xcode 14.2
Your suggestions on what's going on are welcome. And please tell me if I'm doing something your not supposed to be doing with SwiftUI.
Thanks!

Related

SwiftUI 4 NavigationLink is invoking the link twice

I'm using the new NavigationLink in iOS 16 and hitting a problem where the target of the .navigationDestination is being called twice. Example code:
struct Content: View {
var body: some View {
NavigationStack {
ScrollView {
VStack {
ForEach(0..<10) { number in
NavigationLink(value: number) {
Text("\(number)")
}
}
}
}.navigationDestination(for: Int.self) { value in
SubView(model: Model(value: value))
}
}
}
}
class Model: ObservableObject {
#Published var value: Int
init(value: Int) {
print("Init for value \(value)")
self.value = value
}
}
struct SubView: View {
#ObservedObject var model: Model
var body: some View {
Text("\(model.value)")
}
}
When I touch one of the numbers in the Content view the init message in the model is shown twice, indicating that the class has been instantiated more than once. This is not a problem in a trivial example like this, but in my app the model does a network fetch and calculation so this is being done twice, which is more of an issue.
Any thoughts?
I think problem is caused by ObservableObject. It needs to store it's state. I think it's better to use StateObject. Please try that one for SubView. :)
struct SubView: View {
#StateObject private var model: Model
init(value: Int) {
self._model = StateObject(wrappedValue: Model(value: value))
}
var body: some View {
Text("\(model.value)")
}
}

Nesting of several NavigationLink in a NavigationStack causes the loss of animation and breaks the backtracking

SwiftUI 4.0 introduces a new NavigationStack view.
Let's consider this simple structure.
struct Item: Identifiable, Hashable {
static let sample = [Item(), Item(), Item()]
let id = UUID()
}
When a NavigationLink is nested in another one, the navigation loses its animation and the backtracking takes directly to the root. Did I miss something, or is this a bug?
struct ItemDetailView: View {
let item: Item
var body: some View {
Text(item.id.uuidString)
}
}
struct ItemListView: View {
var body: some View {
List(Item.sample) { item in
NavigationLink(item.id.uuidString, value: item)
}
}
}
struct ExploreView: View {
var body: some View {
List {
Section {
NavigationLink {
ItemListView()
} label: {
Text("Items")
}
}
}
.navigationTitle("Explore")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
ExploreView()
}
}
}
Thanks!
Found the solution thanks to #Asperi's comment.
First, create a Hashable enum containing the destinations.
enum Destination: Hashable {
case items
var view: some View {
switch self {
case .items:
return ItemListView()
}
}
var title: LocalizedStringKey {
switch self {
case .items:
return "Items"
}
}
}
Next, use the new NavigationLink initializer.
NavigationLink(Destination.items.title, value: Destination.items)
And finally, add a new .navigationDestination modifier to catch all Destination values.
.navigationDestination(for: Destination.self) { destination in
destination.view
}

Satisfying var body: some View in protocol extension

I wish to do the following:
import SwiftUI
protocol CombinedView: View {
var dataForViewA: String { get }
var viewA: any View { get }
var viewB: any View { get }
}
extension CombinedView {
var viewA: Text {
Text(dataForViewA)
}
var body: some View {
VStack {
viewA
viewB
}
}
}
viewA works fine because I can specify the concrete type, but var body complains:
Type 'any View' cannot conform to 'View'
I am unsure what I need to implement to solve this. Any ideas?
Thanks in advance for any advice
It does not work that way, instead we need associated types for each generic view and specialisation in extension.
Here is fixed variant. Tested with Xcode 14b3 / iOS 16
protocol CombinedView: View {
associatedtype AView: View
associatedtype BView: View
var dataForViewA: String { get }
var viewA: Self.AView { get }
var viewB: Self.BView { get }
}
extension CombinedView {
var body: some View {
VStack {
viewA
viewB
}
}
}
extension CombinedView where AView == Text {
var viewA: Text {
Text(dataForViewA)
}
}
Test module on GitHub

How to define a generic SwiftUI view that can accept sectioned fetch results for different entities

I need to define a generic SwiftUI view that can accept sectioned fetch results for different CoreData entities, but I'm not sure how to define the generic view.
In the example below, I have two sectioned fetch results defined for Patient and Doctor entities. I need to be able to pass them to the generic view.
#SectionedFetchRequest(
sectionIdentifier: \.sectionTitle,
sortDescriptors: Patient.nameSortDescriptors(), animation: .default)
private var patients: SectionedFetchResults<String, Patient>
#SectionedFetchRequest(
sectionIdentifier: \.sectionTitle,
sortDescriptors: Doctor.nameSortDescriptors(), animation: .default)
private var doctors: SectionedFetchResults<String, Doctor>
GenericView(items: patients)
GenericView(items: doctors)
struct GenericView: View {
let items: ?????
}
One way is to supply not only the fetched results but also the view to use for each object in the results.
The below view is generic for the objects to display, Object, and the view to use, Content, for each Object. In this example I am displaying a list of all objects
struct GenericView<Object: NSManagedObject, Content: View>: View {
let items: SectionedFetchResults<String, Object>
let rowContent: (Object) -> Content
init(items: SectionedFetchResults<String, Object>, rowContent: #escaping (Object) -> Content) {
self.items = items
self.rowContent = rowContent
}
var body: some View {
List {
ForEach(items) { section in
Section(header: Text(section.id)) {
ForEach(section, id: \.objectID) { item in
rowContent(item)
}
}
}
}
}
}
Then you call the view like this for eaxample
GenericView(items: patients, rowContent: { patient in
Text(patient.name)
})
If for example Doctor and Patient conforms to protocol Human, then it could be as follows
protocol Human {
var name: String { get set }
// other code here
}
class Doctor: NSManagedObject, Human {
var name: String = ""
// other code here
}
struct GenericView<T: NSManagedObject & Human>: View {
let items: SectionedFetchResults<String, T>
var body: some View {
// other code here
}
}

How do I pass a value to a ViewModel from a View using the .environmentObject() in Swift?

I have created a ViewModel with an init() that accepts a parameter something like this. PS: Learning swift and swiftUI
//UsersViewModel.swift
class UsersViewModel: ObservableObject {
#Published var users: [User]
#Published var category: String
init(category: String) {
self.category = continentcategory
self.users = UserData().getUsers(byCategory: category)
}
}
UserData is the Data Model where I have a function getUsers(byCategory) that allows me to get a subset of data instead of all data and then filtering it.
For my SwiftUI view
//UserListByCategory.swift
import SwiftUI
struct UserListByCategory: View {
#EnvironmentObject var ud: UsersViewModel
var body: some View {
Text("Hello")
}
}
struct UserListByCategory_Previews: PreviewProvider {
static var previews: some View {
UserListByCategory()
.environmentObject(UsersViewModel(category: "Office"))
}
}
This above SwiftUI View gets called by another ListView after the user selects a category. How do I pass that category without hardcoding it here?
In SwiftUI we don't use view model objects for our view data. View structs are our primary encapsulation mechanism, e.g.
If you need to fetch your data:
struct UserListByCategory: View {
let category: String
#State var users: [User] = []
var body: some View {
List {
ForEach(users) { user in
Text("\(user.name)")
}
}
.task(id: category) {
users = await fetchUsers(category: category)
}
}
}
struct UserListByCategory_Previews: PreviewProvider {
static var previews: some View {
UserListByCategory(category: "Office")
}
}
If you already have all the model data, pass it down the View struct hierarchy as follows:
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
UserList(users: model.users(category:"Office"))
}
}
struct UserList: View {
let users: [User]
var body: some View {
List {
ForEach(users) { user in
Text("\(user.name)")
}
}
}
}