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 ObservableObject
s 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
Ios11 Swift Silent Push (Background Fetch, Didreceiveremotenotification) Is Not Working Anymore
How to Access Nswindow from @Main App Using Only Swiftui
Navigationview Pops Back to Root, Omitting Intermediate View
Show Am/Pm in Capitals in Swift
How to Draw Text in PDF Context in Swift
How to Do If Pattern Matching with Multiple Cases
How to Change Back Button Title on Navigation Controller in Swift3
Swift Custom Context Menu Previewprovider Can Not Click Any View Inside(Using Tapgesture)
What Versions of Swift Are Supported by What Versions of Xcode
Interface Builder, @Iboutlet and Protocols for Delegate and Datasource in Swift
Saving Webview to Pdf Returns Blank Image
Take a Snapshot of Current Screen with Metal in Swift
Fixing Nsurlconnection Deprecation from Swift 1.2 to 2.0
How to Check If a Property Value Exists in Array of Objects in Swift
How to Get Directory Size with Swift on Os X
Variable 'Xxx' Was Never Mutated, Consider Changing to 'Let'
Swift - Could Not Cast Value of Type '_Nscfstring' to 'Nsdictionary'