Ok I spent some time figuring these out, but once I got it right, everything makes sense.
At the moment it is not possible to use PropertyWrappers in protocols. But what you can do is use generics in your View and expect your ViewModels to comply to your protocol. This is specially great if you are testing things or you need to setup something lightweight for the Preview.
I have some sample example here so you can get yours right
Protocol:
protocol UploadStoreProtocol:ObservableObject {
var uploads:[UploadModel] {get set}
}
ViewModel:
You want to make sure your view model is ObservableObject
and add @Published
to variables that can change
// For Preview
class SamplePreviewStore:UploadStoreProtocol {
@Published var uploads:[UploadModel] = []
init() {
uploads.append( UploadModel(id: "1", fileName: "Image 1", progress: 0, started: true, errorMessage: nil))
uploads.append( UploadModel(id: "2", fileName: "Image 2", progress: 47, started: true, errorMessage: nil))
uploads.append( UploadModel(id: "3", fileName: "Image 3", progress: 0, started: false, errorMessage: nil))
}
}
// Real Storage
class UploadStorage:UploadStoreProtocol {
@Published var uploads:[UploadModel] = []
init() {
uploads.append( UploadModel(id: "1", fileName: "Image 1", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "2", fileName: "Image 2", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "3", fileName: "Image 3", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "4", fileName: "Image 4", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "5", fileName: "Image 5", progress: 0, started: false, errorMessage: nil))
}
func addItem(){
uploads.append( UploadModel(id: "\(Int.random(in: 100 ... 100000))", fileName: "Image XX", progress: 0, started: false, errorMessage: nil))
}
func removeItemAt(index:Int){
uploads.remove(at: index)
}
}
For the UI View you can use generics:
struct UploadView<ViewModel>: View where ViewModel:UploadStoreProtocol {
@ObservedObject var store:ViewModel
var body: some View {
List(store.uploads.indices){ item in
ImageRow(item: $store.uploads[item])
}.padding()
}
}
struct ImageRow: View {
@Binding var item:UploadModel
var body: some View {
HStack{
Image(item.id ?? "")
.resizable()
.frame(width: 50.0, height: 50.0)
VStack (alignment: .leading, spacing: nil, content: {
Text(item.fileName ?? "-")
Text(item.errorMessage ?? "")
.font(.caption)
.foregroundColor(.red)
})
Spacer()
VStack {
if (item.started){
Text("\(item.progress)").foregroundColor(.purple)
}
UploadButton(is_started: $item.started)
}
}
}
}
Now your view is ready to get the ViewModel, you can have your store setup externally like this:
@main
struct SampleApp: App {
@StateObject var uploadStore = UploadStorage()
var body: some Scene {
WindowGroup {
UploadView(store: uploadStore)
}
}
}
and for Preview you can have:
struct ContentView_Previews: PreviewProvider {
@StateObject static var uploadStore = SamplePreviewStore()
static var previews: some View {
UploadView(store: uploadStore)
}
}