'SwiftUI: Preview with data in ViewModel
I load my data from a viewModel which is loading data from web. Problem: I want to set some preview sample data to have content in preview window. Currently my preview contains an empty list as I do not provide data.
How can I achieve this?
struct MovieListView: View {
@ObservedObject var viewModel = MovieViewModel()
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView()
}
}
class MovieViewModel: ObservableObject{
private let provider = NetworkManager()
@Published var movies = [Movie]()
init() {
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)}
}
}
Solution 1:[1]
Further to the answer above, and if you want to keep your shipping codebase clean, I've found that extending the class captured in PreProcessor flags to add a convenience init works.
#if DEBUG
extension MovieViewModel{
convenience init(forPreview: Bool = true) {
self.init()
//Hard code your mock data for the preview here
self.movies = [Movie(...)]
}
}
#endif
Then modify your SwiftUI structs using preprocessor flags as well:
struct MovieListView: View {
#if DEBUG
let viewModel: MovieViewModel
init(viewModel: MovieViewModel = MovieViewModel()){
self.viewModel = viewModel
}
#else
@StateObject var viewModel = MovieViewModel()
#endif
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView(viewModel: MovieViewModel(forPreview: true)
}
}
Solution 2:[2]
This question was written before @StateObject
was introduced at WWDC 2020. I believe these days you'd want to use @StateObject
instead of @ObservedObject
because otherwise your view model can be re-initialized numerous times (which would result in multiple network calls in this case).
I wanted to do the exact same thing as OP, but with @StateObject
. Here's my solution that doesn't rely on any build configurations.
struct MovieListView: View {
@StateObject var viewModel = MovieViewModel()
var body: some View {
MovieListViewInternal(viewModel: viewModel)
}
}
private struct MovieListViewInternal<ViewModel: MovieViewModelable>: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
List {
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
}
}
.onAppear {
viewModel.fetchMovieRatings()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListViewInternal(viewModel: PreviewMovieViewModel())
}
}
The View model protocols and implementations:
protocol MovieViewModelable: ObservableObject {
var movies: [Movie] { get }
func fetchMovieRatings()
// Define vars or funcs for anything else your view accesses in your view model
}
class MovieViewModel: MovieViewModelable {
@Published var movies = [Movie]()
init() {
loadNewMovies()
}
private func loadNewMovies() {
// do the network call
}
func fetchMovieRatings() {
// do the network call
}
}
class PreviewMovieViewModel: MovieViewModelable {
@Published var movies = [fakeMovie1, fakeMovie2]
func fetchMovieRankings() {} // do nothing while in a Preview
}
This way your external interface to MovieListView
is exactly the same, but for your previews you can use the internal view definition and override the view model type.
Solution 3:[3]
So while @Kramer's solution works, I hit a challenge with it in the sense that when I would debug the app on my device it would load the preview data and not other "development" data that I would be wanting to be using.
So I extended the solution a little by creating a new build configuration called "Preview" and then wrapped all the 'preview' related data into that build configuration.
That gives me the option then to preview dummy data in the Xcode preview, while still allowing me then to build and debug a development build with development data on my devices/simulators.
So my solution now looks like this..
class MovieViewModel: ObservableObject {
init() {
#if PREVIEW
//Hard code your mock data for the preview here
self.movies = [Movie(...)]
#else
// normal init stuff here
#endif
}
}
struct MovieListView: View {
#if PREVIEW
let viewModel: MovieViewModel
init(viewModel: MovieViewModel = MovieViewModel()){
self.viewModel = viewModel
}
#else
@StateObject var viewModel = MovieViewModel()
#endif
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView(viewModel: MovieViewModel())
}
}
Might not be the best crack at this, but gave me the flexibility to manage my Preview dummy data separate to my development/Debug data and has so far proven to work well for my use cases so far. :)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Jeremy |
Solution 3 | Simon |