Previewprovider and Observedobject Properties

PreviewProvider and ObservedObject properties

If you are using the standard PersistenceController that comes with Xcode when you start a new project with CoreData just add the below method so Xcode returns the .preview container when you are running in preview.

    public static func previewAware() -> PersistenceController{
//Identifies if XCode is running for previews
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"{
return PersistenceController.preview
}else{
return PersistenceController.shared
}
}

As for the rest you can use something like this.

import SwiftUI
import CoreData

struct SamplePreviewView: View {
@ObservedObject var item: Item
var body: some View {
Text(item.timestamp?.description ?? "nil")
}
}
struct SamplePreviewView_Previews: PreviewProvider {
static let svc = CoreDataPersistenceService()
static var previews: some View {
SamplePreviewView(item: svc.addSample())
}
}

class CoreDataPersistenceService: NSObject {

var persistenceController: PersistenceController

init(isTest: Bool = false) {
if isTest{
self.persistenceController = PersistenceController.preview
}else{
self.persistenceController = PersistenceController.previewAware()
}
super.init()
}
func addSample() -> Item {
let object = createObject()
object.timestamp = Date()
return object
}
//MARK: CRUD methods
func createObject() -> Item {
let result = Item.init(context: persistenceController.container.viewContext)
return result
}
}

SwiftUI View don't see property of ObservableObject marked with @Published

Some things to note.

You can't chain ObservableObjects so @ObservedObject var viewModel = BeersListViewModel() inside the class won't work.

The second you have 2 ViewModels one in the View and one in the Presenter you have to pick one. One will not know what the other is doing.

Below is how to get your code working

import SwiftUI
struct Beer: Identifiable{
var id: UUID = UUID()
var name: String = "Hops"
var abv: String = "H"
}
protocol BeersListInteractorProtocol{
func loadList(at: Int, completion: ([Beer])->Void)
}

struct BeersListInteractor: BeersListInteractorProtocol{
func loadList(at: Int, completion: ([Beer]) -> Void) {
completion([Beer(), Beer(), Beer()])
}
}
protocol BeersListPresenterProtocol: ObservableObject{
var interactor: BeersListInteractorProtocol { get set }
var viewModel : BeersListViewModel { get set }

func formattedABV(_ abv: String) -> String
func loadList(at page: Int)
}
class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
var interactor: BeersListInteractorProtocol
//You can't chain `ObservedObject`s
@Published var viewModel = BeersListViewModel()

init(interactor: BeersListInteractorProtocol){
self.interactor = interactor

}
func loadList(at page: Int){
interactor.loadList(at: page) { beers in
DispatchQueue.main.async {
self.viewModel.beers.append(contentsOf: beers)
print(self.viewModel.beers)
}
}
}
func formattedABV(_ abv: String) -> String{
"**\(abv)**"
}
}

//Change to struct
struct BeersListViewModel{
var beers = [Beer]()
}


struct BeerListView<T: BeersListPresenterProtocol>: View{
//This is what will trigger view updates
@StateObject var presenter : T
//The viewModel is in the Presenter
//@StateObject var viewModel : BeersListViewModel
var body: some View {
NavigationView{
List{
Button("load list", action: {
presenter.loadList(at: 1)
})
ForEach(presenter.viewModel.beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(presenter.formattedABV(beer.abv))")
.font(.subheadline)

}
}
}
}
}
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView(presenter: BeersListPresenter(interactor: BeersListInteractor()))
}
}

Now I am not a VIPER expert by any means but I think you are mixing concepts. Mixing MVVM and VIPER.Because in VIPER the presenter Exists below the View/ViewModel, NOT at an equal level.

I found this tutorial a while ago. It is for UIKit but if we use an ObservableObject as a replacement for the UIViewController and the SwiftUI View serves as the storyboard.

It makes both the ViewModel that is an ObservableObject and the View that is a SwiftUI struct a single View layer in terms of VIPER.

You would get code that looks like this

protocol BeersListPresenterProtocol{
var interactor: BeersListInteractorProtocol { get set }

func formattedABV(_ abv: String) -> String
func loadList(at: Int, completion: ([Beer]) -> Void)

}
struct BeersListPresenter: BeersListPresenterProtocol{
var interactor: BeersListInteractorProtocol

init(interactor: BeersListInteractorProtocol){
self.interactor = interactor

}
func loadList(at: Int, completion: ([Beer]) -> Void) {

interactor.loadList(at: at) { beers in
completion(beers)
}
}
func formattedABV(_ abv: String) -> String{
"**\(abv)**"
}
}
protocol BeersListViewProtocol: ObservableObject{
var presenter: BeersListPresenterProtocol { get set }
var beers: [Beer] { get set }
func loadList(at: Int)
func formattedABV(_ abv: String) -> String

}
class BeersListViewModel: BeersListViewProtocol{
@Published var presenter: BeersListPresenterProtocol
@Published var beers: [Beer] = []

init(presenter: BeersListPresenterProtocol){
self.presenter = presenter
}
func loadList(at: Int) {
DispatchQueue.main.async {
self.presenter.loadList(at: at, completion: {beers in
self.beers = beers
})
}
}

func formattedABV(_ abv: String) -> String {
presenter.formattedABV(abv)
}
}
struct BeerListView<T: BeersListViewProtocol>: View{
@StateObject var viewModel : T

var body: some View {
NavigationView{
List{
Button("load list", action: {
viewModel.loadList(at: 1)
})
ForEach(viewModel.beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(viewModel.formattedABV(beer.abv))")
.font(.subheadline)

}
}
}
}
}
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView(viewModel: BeersListViewModel(presenter: BeersListPresenter(interactor: BeersListInteractor())))
}
}

If you don't want to separate the VIPER View Layer into the ViewModel and the SwiftUI View you can opt to do something like the code below but it makes it harder to replace the UI and is generally not a good practice. Because you WON'T be able to call methods from the presenter when there are updates from the interactor.

struct BeerListView<T: BeersListPresenterProtocol>: View, BeersListViewProtocol{
var presenter: BeersListPresenterProtocol
@State var beers: [Beer] = []

var body: some View {
NavigationView{
List{
Button("load list", action: {
loadList(at: 1)
})
ForEach(beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(formattedABV(beer.abv))")
.font(.subheadline)

}
}
}
}
}
}
func loadList(at: Int) {
DispatchQueue.main.async {
self.presenter.loadList(at: at, completion: {beers in
self.beers = beers
})
}
}

func formattedABV(_ abv: String) -> String {
presenter.formattedABV(abv)
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView<BeersListPresenter>(presenter: BeersListPresenter(interactor: BeersListInteractor()))
}
}

SwiftUI: Preview with data in ViewModel

Here is possible approach (based on dependency-injection of view model members instead of tight-coupling)

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
// create Movie to be previewed inline, say from bundled data
MovieListView(viewModel: MovieViewModel(provider: nil, movies: [Movie(...)]))
}
}

class MovieViewModel: ObservableObject {

private var provider: NetworkManager?

@Published var movies: [Movie]

// same as before by default, but allows to modify if/when needed explicitly
init(provider: NetworkManager? = NetworkManager(), movies: [Movie] = []) {
self.provider = provider
self.movies = movies

loadNewMovies()
}

func loadNewMovies(){
provider?.getNewMovies(page: 1) {[weak self] movies in
print("\(movies.count) new movies loaded")
self?.movies.removeAll()
self?.movies.append(contentsOf: movies)
}
}
}

SwiftUI Preview doesn't work with Core Data

import SwiftUI
import CoreData

struct FishDetailView: View {

@ObservedObject var item: Item

var body: some View {
Text(item.title ?? "")
...
// fyi your image should be a transformable attribute on the Item entity
}
}

struct PreviewTestView_Previews: PreviewProvider {

static var firstItem: Item? {
let context = PersistenceController.preview.container.viewContext
let fetchRequest = Item.fetchRequest()
fetchRequest.fetchLimit = 1
let results = try? context.fetch(fetchRequest)
return results?.first
}

static var previews: some View {
if let firstItem {
FishDetailView(item: firstItem)
}
else {
Text("Item not found")
}
}
}

How can I use a variable set in one View in the PreviewProvider for that View?

Separating model management into view model and using dependency injection give possibility to mock view model for preview.

Here is a demo of possible approach. Tested with Xcode 12 / iOS 14

struct Home { // just replicated for test
var homeName: String
}

class HomesViewModel: ObservableObject {
@Published var homesList: [Home]

init(homes: [Home] = []) { // default container
self.homesList = homes
}

func fetchHomes() {
guard homesList.isEmpty else { return }

retrieveHomes(completionHandler: { (json) in // load if needed
DispatchQueue.main.async {
self.homesList = json // should be set on main queue
}
})
}
}

struct HomeRow: View {
var home: Home
@ObservedObject var vm: HomesViewModel

var body: some View {
HStack {
Text(home.homeName)
Spacer()
}
.onAppear {
self.vm.fetchHomes()
}
}
}


struct HomeRow_Previews: PreviewProvider {
static var previews: some View {

// prepare mock model with predefined mock homes
let mockModel = HomesViewModel(homes: [Home(homeName: "Mock1"), Home(homeName: "Mock2")])

return Group {
// inject test model via init
HomeRow(home: mockModel.homesList[0], vm: mockModel)
}
.previewLayout(.fixed(width: 300, height: 70))
}
}

if statement is using data from PreviewProvider

It looks like you need to apply the check on the ForEach item and not on self.weeklyrunclub:

ForEach(runClubData.filter {
searchText.isEmpty ? true : $0.name.contains(searchText)
}) { item in
if item.category == "Monday" { // replace `self.weeklyrunclub` with `item`
ClubViews(weeklyrunclub: item)
}
}

Can a published var in observed object be used directly in Picker selection as binding var?

The same notation, ie $, for ObservedObject properties,

Picker(selection: $settingsStore.dataType, label: Text("Dafault Data Type")) {
Text("Blood Ketone Value (mmol/L)").tag(0)
Text("Ketostix").tag(1)
}.pickerStyle(SegmentedPickerStyle())


Related Topics



Leave a reply



Submit