5

Hi I was just wondering is it possible to create a generic class confirming to ObservableObject protocol which can be used by more then one ContentViews.

If i can do that then I will be able make my ContentView and Model class completely generic and reusable.

An example of what i would like to achieve:

protocol ContentViewModelType: ObservableObject {
    var propertyToInitialiseView: [String] { get }
}

struct ContentView: View {
    @ObservedObject var viewModel: some ViewModel

    var body: some View {
        Text("Hello World")
    }
}

If I can do that any class can implement ContentViewModelType and become a model for ContentView which makes it generic and reusable. For example

class ViewModel: ObservableObject {
    var objectWillChange = PassthroughSubject<ViewModel, Never>()
}

But when i try to initialise ContentView that xcode gives me a type error.

enter image description here

I thought the whole point of introducing some keyword was so that we can use protocol as type for those protocols that have associated type as a requirement and hence this should work. But it gives an error.

If anyone has any references or knowledge about this problem that they could share or possibly a solution for this it would be great.

Thanks in advance.

nishith Singh
  • 2,968
  • 1
  • 15
  • 25
  • `protocol ContentViewModelType: ObservableObject`... correct me, but you cannot do this - `ObservableObject` is *always* a `class` object. –  Sep 24 '19 at 20:55
  • `dfd` it's fine for a protocol to inherit from another protocol. It just means "this protocol requires all the things of my ancestor, and also…" You're correct that `ContentViewModelType` doesn't *conform* to `ObservableObject`. Unfortunately "inherits from" and "conforms to" are spelt identically in Swift. – Rob Napier Sep 25 '19 at 15:51
  • This technique is demonstrated in WWDC 2020 Structure your app for SwiftUI previews from 21:13 https://developer.apple.com/videos/play/wwdc2020/10149/?time=1273 – malhal Oct 23 '20 at 16:36

3 Answers3

9

Trying to understand your question, and I am not quite sure I understand the intent... but to create a view which takes in generic view model (based on the protocol you had before) you will need the following code:

protocol ViewModelWithProperties: ObservableObject {
    var properties: [String] { get }
}

struct SomeView<T>: View where T: ViewModelWithProperties {
// this can also be written as 
// struct SomeView<T: ViewModelWithProperties>: View {
    @ObservedObject var item: T

    var body: some View {
        VStack {
            ForEach(item.properties, id: \.self) {
                Text($0)
            }
        }
    }
}

To consume this instance you will need to:

struct ContentView: View {
    var body: some View {
        SomeView(item: MyViewModel())
    }
}

As stated in one of the other answers, some is used for opaque types, it doesn't make your code generic.

rodrigoelp
  • 2,550
  • 1
  • 19
  • 29
3

This is not what some is for. some creates an opaque return type, not an existential ("any") type. See the docs for more detail. See also What is the `some` keyword in SwiftUI?

A some type must be a single type, known at compile time. It's just not known to the caller. What you're trying to do is pass an existential, which is a type known at runtime. Nothing has changed in Swift 5.1 about existentials. You would still need to wrap this up in an AnyContentViewModel if that's what you wanted. (I'll need to think a bit on whether that would be a good idea or not.)

But the code as you've written it also doesn't do what you're describing. You're not actually using ContentViewModelType anywhere. Did you mean some ContentViewModelType? That still won't work, but it seems what you mean.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
-2

If I'm understanding you correctly you could do something like this:

You could have a an ObservableObject class as follows:

import Foundation

class SampleTimer: ObservableObject {
    @Published var timerVar: Int = 0

    init(){
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true){
            timer in
            DispatchQueue.main.async {
                self.timerVar += 1
            }
        }
    }
}

a ContentView:

import SwiftUI

struct ContentView: View {
    @ObservedObject var sampleTimer = SampleTimer()
    var body: some View {
        NavigationView {
            VStack {
                Text("\(sampleTimer.timerVar)")
                NavigationLink(destination: SecondView(sampleTimer: sampleTimer) ){
                    Text("Go to second view")
                }
            }
        }
    }
}

and a SecondView like:

import SwiftUI

struct SecondView: View {
    @ObservedObject var sampleTimer : SampleTimer
    var body: some View {
        VStack {
            Text("\(sampleTimer.timerVar)")
        }
    }
}

The important thing that I wanted to outline was that if you have a timer like I do and it's initialized in the init() function AND you create a new instance of SampleTimer() in the SecondView (i.e. @ObservedObject var sampleTimer = SampleTimer() instead of what I have) instead of getting the same instance passed down from the ContentView() to the SecondView() you will have two different instances of SampleTimer meaning that the values will not be the same in the ContentView and the SecondView

39fredy
  • 1,923
  • 2
  • 21
  • 40