How to make a SwiftUI View a property of a Protocol - swift

I'm trying to extend a protocol so that a certain few impls of the protocol have a view associated with them. However, because a SwiftUI View is a protocol, this is proving to be a challenge.
import SwiftUI
protocol ParentProtocol {
var anyProperty: String { get }
}
protocol ChildProtocol : ParentProtocol {
associatedtype V
var someView: V { get }
}
class ChildImpl : ChildProtocol {
var someView : some View {
Text("Hello World")
}
var anyProperty: String = ""
}
class ChildMgr {
var child: ParentProtocol = ChildImpl()
func getView() -> some View {
guard let child = child as? ChildProtocol else { return EmptyView() }
return child.someView
}
}
Its not clear to me where to constrain the ChildProtocol's associated type to a View (or Text for that matter).
At the guard let child = ... I get the following compiler error:
Protocol 'ChildProtocol' can only be used as a generic constraint because it has Self or associated type requirements
and when returning the chid's view I get:
Member 'someView' cannot be used on value of protocol type 'ChildProtocol'; use a generic constraint instead
I think the answer may be in this thread: https://developer.apple.com/forums/thread/7350
but frankly its confusing on how to apply it to this situation.

Don't use runtime checks. Use constrained extensions.
I also don't see a reason for you to be using classes.
protocol ChildProtocol: ParentProtocol {
associatedtype View: SwiftUI.View
var someView: View { get }
}
final class ChildImpl: ChildProtocol {
var someView: some View {
Text("Hello World")
}
var anyProperty: String = ""
}
final class ChildMgr<Child: ParentProtocol> {
var child: Child
init(child: Child) {
self.child = child
}
}
extension ChildMgr where Child: ChildProtocol {
func getView() -> some View {
child.someView
}
}
extension ChildMgr {
func getView() -> some View {
EmptyView()
}
}
extension ChildMgr where Child == ChildImpl {
convenience init() {
self.init(child: .init())
}
}

Related

SwiftUI - MVVM - nested components (array) and bindings understanding problem

I just stuck trying to properly implement MVVM pattern in SwiftUI
I got such application
ContainerView is the most common view. It contains single business View Model object - ItemsViewModel and one surrogate - Bool to toggle it's value and force re-render of whole View.
ItemsView contains of array of business objects - ItemView
What I want is to figure out:
how to implement bindings which actually works❓
call back event and pass value from child view to parent❓
I came from React-Redux world, it done easy there. But for the several days I cant figure out what should I do... I chose MVVM though as I also got some WPF experience and thought it'll give some boost, there its done with ObservableCollection for bindable arrays
ContainerViewModel.swift⤵️
final class ContainerViewModel: ObservableObject {
#Published var items: ItemsViewModel;
// variable used to refresh most common view
#Published var refresh: Bool = false;
init() {
self.items = ItemsViewModel();
}
func buttonRefresh_onClick() -> Void {
self.refresh.toggle();
}
func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemViewModel())
}
}
ContainerView.swift⤵️
struct ContainerView: View {
// enshure that enviroment creates single View Model of ContainerViewModel with StateObject for ContainerView
#StateObject var viewModel: ContainerViewModel = ContainerViewModel();
var body: some View {
ItemsView(viewModel: $viewModel.items).padding()
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item from ContainerView")
}
Button(action: viewModel.buttonRefresh_onClick) {
Text("Refresh")
}.padding()
}
}
ItemsViewModel.swift⤵️
final class ItemsViewModel: ObservableObject {
#Published var items: [ItemViewModel] = [ItemViewModel]();
init() {
}
func buttonAddItem_onClick() -> Void {
self.items.append(ItemViewModel());
}
}
ItemsView.swift⤵️
struct ItemsView: View {
#Binding var viewModel: ItemsViewModel;
var body: some View {
Text("Items quantity: \(viewModel.items.count)")
ScrollView(.vertical) {
ForEach($viewModel.items) { item in
ItemView(viewModel: item).padding()
}
}
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item form ItemsView")
}
}
}
ItemViewModel.swift⤵️
final class ItemViewModel: ObservableObject, Identifiable, Equatable {
//implementation of Identifiable
#Published public var id: UUID = UUID.init();
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
// business property
#Published public var intProp: Int;
init() {
self.intProp = 0;
}
func buttonIncrementIntProp_onClick() -> Void {
self.intProp = self.intProp + 1;
}
func buttonDelete_onClick() -> Void {
//todo ❗ I want to delete item in parent component
}
}
ItemView.swift⤵️
struct ItemView: View {
#Binding var viewModel: ItemViewModel;
var body: some View {
HStack {
Text("int prop: \(viewModel.intProp)")
Button(action: viewModel.buttonIncrementIntProp_onClick) {
Image(systemName: "plus")
}
Button(action: viewModel.buttonDelete_onClick) {
Image(systemName: "trash")
}
}.padding().border(.gray)
}
}
I read official docs and countless SO topics and articles, but nowhere got solution for exact my case (or me doing something wrong). It only works if implement all UI part in single view
UPD 1:
Is it even possible to with class but not a struct in View Model?
Updates works perfectly if I use struct instead of class:
ItemsViewModel.swift⤵️
struct ItemsViewModel {
var items: [ItemViewModel] = [ItemViewModel]();
init() {
}
mutating func buttonAddItem_onClick() -> Void {
self.items.append(ItemViewModel());
}
}
ItemViewModel.swift⤵️
struct ItemViewModel: Identifiable, Equatable {
//implementation of Identifiable
public var id: UUID = UUID.init();
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
// business property
public var intProp: Int;
init() {
self.intProp = 0;
}
mutating func buttonIncrementIntProp_onClick() -> Void {
self.intProp = self.intProp + 1;
}
func buttonDelete_onClick() -> Void {
}
}
But is it ok to use mutating functions? I also tried to play with Combine and objectWillChange, but unable to make it work
UPD 2
Thanks #Yrb for response. With your suggestion and this article I came added Model structures and ended up with such results:
ContainerView.swift⤵️
struct ContainerView: View {
// enshure that enviroment creates single View Model of ContainerViewModel with StateObject for ContainerView
#StateObject var viewModel: ContainerViewModel = ContainerViewModel();
var body: some View {
ItemsView(viewModel: ItemsViewModel(viewModel.items)).padding()
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item from ContainerView")
}
}
}
ContainerViewModel.swift⤵️
final class ContainerViewModel: ObservableObject {
#Published var items: ItemsModel;
init() {
self.items = ItemsModel();
}
#MainActor func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemModel())
}
}
ContainerModel.swift⤵️
struct ContainerModel {
public var items: ItemsModel;
}
ItemsView.swift⤵️
struct ItemsView: View {
#ObservedObject var viewModel: ItemsViewModel;
var body: some View {
Text("Items quantity: \(viewModel.items.items.count)")
ScrollView(.vertical) {
ForEach(viewModel.items.items) { item in
ItemView(viewModel: ItemViewModel(item)).padding()
}
}
Button(action: {
viewModel.buttonAddItem_onClick()
}) {
Text("Add item form ItemsView")
}
}
}
ItemsViewModel.swift⤵️
final class ItemsViewModel: ObservableObject {
#Published var items: ItemsModel;
init(_ items: ItemsModel) {
self.items = items;
}
#MainActor func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemModel());
}
}
ItemsModel.swift⤵️
struct ItemsModel {
public var items: [ItemModel] = [ItemModel]();
}
ItemView.swift⤵️
struct ItemView: View {
#StateObject var viewModel: ItemViewModel;
var body: some View {
HStack {
Text("int prop: \(viewModel.item.intProp)")
Button(action: {
viewModel.buttonIncrementIntProp_onClick()
}) {
Image(systemName: "plus")
}
Button(action: {
viewModel.buttonDelete_onClick()
}) {
Image(systemName: "trash")
}
}.padding().border(.gray)
}
}
ItemViewModel.swift⤵️
final class ItemViewModel: ObservableObject, Identifiable, Equatable {
//implementation of Identifiable
#Published private(set) var item: ItemModel;
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
init(_ item: ItemModel) {
self.item = item;
self.item.intProp = 0;
}
#MainActor func buttonIncrementIntProp_onClick() -> Void {
self.item.intProp = self.item.intProp + 1;
}
#MainActor func buttonDelete_onClick() -> Void {
}
}
ItemModel.swift⤵️
struct ItemModel: Identifiable {
//implementation of Identifiable
public var id: UUID = UUID.init();
// business property
public var intProp: Int;
init() {
self.intProp = 0;
}
}
This code runs and works perfectly, at least I see no problems. But I'm not shure if I properly initializes and "bind" ViewModels and Models - code looks quite messy. Also I'n not shure I correctly set ObservedObject in ItemsView and StateObject in ItemView. So please check me
We don't need MVVM in SwiftUI, see: "MVVM has no place in SwiftUI."
In SwiftUI, the View struct is already the view model. We use property wrappers like #State and #Binding to make it behave like an object. If you were to actually use objects instead, then you'll face all the bugs and inconsistencies of objects that SwiftUI and its use of value types was designed to eliminate.
I recommend Data Essentials in SwiftUI WWDC 2020 for learning SwiftUI. The first half is about view data and the second half is about model data. It takes a few watches to understand it. Pay attention to the part about model your data with value type and manage its life cycle with a reference type.
It's best to also use structs for your model types. Start with one single ObservableObject to hold the model types (usually arrays of structs) as #Published properties. Use environmentObject to pass the object into the View hierarchy. Use #Binding to pass write access to the structs in the object. Apple's sample ScrumDinger is a good starting point.

Problem binding a subclass to parent class in SwiftUI View

I've been experimenting on trying to binding subclass to a SwiftUI view that takes it parent class as a parameter.
These are my classes:
class Animal {
func sound() -> String {
return ""
}
}
class Cat : Animal {
override func sound() -> String {
return "Meow"
}
func purr() {
print("purring")
}
}
class Dog : Animal {
override func sound() -> String {
return "Woof"
}
func fetch() {
print("fetching")
}
}
Here are the views I have set up.
struct ContentView: View {
#State var creature:Cat = Cat()
var body: some View {
AnimalView(creature: $creature)
}
}
struct AnimalView: View {
#Binding var creature:Animal
var body: some View {
Text(creature.sound())
.padding()
}
}
This results in the compile error:
Cannot convert value of type 'Binding<Cat>' to expected argument type 'Binding<Animal>'
What the proper way to do bind to a view the takes a parent class?
Searching around I think the structure I would want is to make the view itself generic.
struct ContentView: View {
#State var creature:Cat = Cat()
#State var creature2:Dog = Dog()
var body: some View {
VStack {
AnimalView(creature: $creature)
AnimalView(creature: $creature2)
}
}
}
struct AnimalView<T:Animal> : View {
#Binding var creature:T
#State var sound:String = "No Sound"
var body: some View {
Text(creature.name)
.padding()
.onAppear {
self.sound = self.creature.sound()
}
}
}
This will allow me to bind to types that inherit from Animal and let me use one view rather that having to create a separate Cat and Dog view.
Also just in case for better form, here is a same thing with protocol and classes.
protocol Animal {
var name:String {
get
}
func sound() -> String
}
struct Cat : Animal {
var name:String {
get {
return "Cat"
}
}
func sound() -> String {
return "Meow"
}
func purr() {
print("purring")
}
}
struct Dog : Animal {
var name:String {
get {
return "Dog"
}
}
func sound() -> String {
return "Woof"
}
func fetch() {
print("fetching")
}
}

Reference to generic type when returning view that uses a protocol

I am being unable to return a View that uses a protocol as a dependency as this is throwing me Reference to generic type 'LoginView' requires arguments in <...>.
func makeLoginView(viewModel: LoginViewModelType) -> LoginView {
return LoginView(viewModel: viewModel)
}
My LoginView uses LoginViewModelType as I have two different view models.
protocol LoginViewModelType: ObservableObject {
var bookingPaymentViewModel: BookingPaymentViewModel? { get }
var email: String { get set }
var password: String { get set }\
...
func login()
}
struct LoginView<ViewModel: LoginViewModelType>: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
...
}
I can't understand what I am doing wrong, the LoginView should be able to return a View regardless as it complies to LoginViewModelType.
You need to constrain makeLoginView to accept a generic of type LoginViewModelType and then use that same generic in the returned value.
class BookingPaymentViewModel { }
func makeLoginView<T:LoginViewModelType>(viewModel: T) -> LoginView<T> {
return LoginView(viewModel: viewModel)
}
protocol LoginViewModelType: ObservableObject {
var bookingPaymentViewModel: BookingPaymentViewModel? { get }
var email: String { get set }
var password: String { get set }
func login()
}
struct LoginView<ViewModel: LoginViewModelType>: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Text("test")
}
}

Core Data and SwiftUI polymorphism issues

Having troubles putting down together SwiftUI and generic types for handling Core Data.
Consider following example:
Parent is abstract. Foo and Bar are children of Parent and they have some custom attributes.
Now what I want to do, is roughly that:
protocol EntityWithView {
associatedtype T: View
func buildView() -> T
}
extension Parent: EntityWithView {
func buildView() -> some View {
fatalError("Re-implement in child")
}
}
extension Foo {
override func buildView() -> some View {
return Text(footribute)
}
}
extension Bar {
override func buildView() -> some View {
return Text(atrribar)
}
}
struct ViewThatUsesCoreDataAsModel: View {
let entities: [Parent]
var body: some View {
ForEach(entities) { entity in
entity.buildView()
}
}
}
I would want to add polymorphic builder to my core data entities that shape data or build views, that confirm to common interface so I can use them without casting/typing.
Problem that compiler throws errors if I try to modify generated Core data entity directly not through extension, and confirming to protocol though extension doesn't allow overriding.
Ok, this is head-breaking (at least for Preview, which gone crazy), but it works in run-time. Tested with Xcode 11.4 / iOS 13.4.
As we need to do all in extension the idea is to use dispatching via Obj-C messaging, the one actually available pass to override implementation under such requirements.
Note: use Simulator or Device
Complete test module
protocol EntityWithView {
associatedtype T: View
var buildView: T { get }
}
extension Parent {
// allows to use Objective-C run-time messaging by complete
// type erasing.
// By convention subclasses
#objc func generateView() -> Any {
AnyView(EmptyView()) // << safe
//fatalError("stub in base") // << alternate
}
}
extension Parent: EntityWithView {
var buildView: some View {
// restory SwiftUI view type from dispatched message
guard let view = self.generateView() as? AnyView else {
fatalError("Dev error - subview must generate AnyView")
}
return view
}
}
extension Foo {
#objc override func generateView() -> Any {
AnyView(Text(footribute ?? ""))
}
}
extension Bar {
#objc override func generateView() -> Any {
AnyView(Text(attribar ?? ""))
}
}
struct ViewThatUsesCoreDataAsModel: View {
let entities: [Parent]
var body: some View {
VStack {
ForEach(entities, id: \.self) { entity in
entity.buildView
}
}
}
}
struct DemoGeneratingViewInCoreDataExtension: View {
#Environment(\.managedObjectContext) var context
var body: some View {
ViewThatUsesCoreDataAsModel(entities: [
Foo(context: context),
Bar(context: context)
])
}
}

SwiftUI alternate views - Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols

I have two reusable views, Ex1 and Ex2. I am trying to show one of them depends on a condition alternately but I could not it.
ContentvIew:
struct ContentView: View {
#State var selector = false
var cvc = ContentViewController()
var body: some View {
ZStack { // ERROR: Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols
cvc.getView(t: selector)
Button(action: {
self.selector.toggle()
print(self.selector)
}) {
Text("Button")
}
}
}
}
Ex1 :
import SwiftUI
struct Ex1: View {
var body: some View {
Text("Ex 1")
}
}
Ex2 :
import SwiftUI
struct Ex2: View {
var body: some View {
Text("Ex 2")
}
}
ContentViewController :
import Foundation
class ContentViewController {
let a = Ex1()
let b = Ex2()
func getView (t: Bool) ->(Any){
if t {
return a
}
else {
return b
}
}
}
I think it is very simple but not for me, for now. Help for two things, please.
I want to understand this problem, and the solution.
Best way for alternate two view in a layout.
Thanks in advance.
As the error suggests the return type specified in ContentViewController's getView method does not conform to the protocols.
In SwiftUI everything you specified in body{} clause must be a type of View if you do not know what kind of view available at runtime.
You can specify AnyView type for unknown views.
So your error will be removed by changing the ContentViewController's code.
class ContentViewController {
let a = Ex1()
let b = Ex2()
func getView (t: Bool) -> (AnyView) {
if t {
return AnyView(a)
}
else {
return AnyView(b)
}
}
}