SwiftUI: Preview with data in ViewModel - swift

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)}
}
}

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)
}
}
}

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.

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)
}
}

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. :)

I have been struggling with this as well and came up with the following simple solution.
//View
struct MyView: View {
#StateObject private var viewModel = ViewModel()
init(forPreview: Bool = false) {
guard forPreview else { return }
let viewModel = ViewModel()
viewModel.title = "Preview" // Call internal func to load sample data
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
Text(viewModel.title)
}
}
//View Model
extension MyView {
#MainActor class ViewModel: ObservableObject {
#Published var title: String = "Standard"
}
}
//Previews
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(forPreview: true)
}
}
This initialization of #StateObject is Apple approved.

Related

How to trigger automatic SwiftUI Updates with #ObservedObject using MVVM

I have a question regarding the combination of SwiftUI and MVVM.
Before we start, I have read some posts discussing whether the combination of SwiftUI and MVVM is necessary. But I don't want to discuss this here, as it has been covered elsewhere. I just want to know if it is possible and, if yes, how. :)
So here comes the code. I tried to add the ViewModel Layer in between the updated Object class that contains a number that should be updated when a button is pressed. The problem is that as soon as I put the ViewModel Layer in between, the UI does not automatically update when the button is pressed.
View:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#ObservedObject var numberStorage = NumberStorage()
var body: some View {
VStack {
// Text("\(viewModel.getNumberObject().number)")
// .padding()
// Button("IncreaseNumber") {
// viewModel.increaseNumber()
// }
Text("\(numberStorage.getNumberObject().number)")
.padding()
Button("IncreaseNumber") {
numberStorage.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published var number: NumberStorage
init() {
self.number = NumberStorage()
}
func increaseNumber() {
self.number.increaseNumber()
}
func getNumberObject() -> NumberObject {
self.number.getNumberObject()
}
}
Model:
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func getNumberObject() -> NumberObject {
return self.numberObject
}
public func increaseNumber() {
self.numberObject.number+=1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
} ```
Looking forward to your feedback!
I think your code is breaking MVVM, as you're exposing to the view a storage model. In MVVM, your ViewModel should hold only two things:
Values that your view should display. These values should be automatically updated using a binding system (in your case, Combine)
Events that the view may produce (in your case, a button tap)
Having that in mind, your ViewModel should wrap, adapt and encapsulate your model. We don't want model changes to affect the view. This is a clean approach that does that:
View:
struct ContentView: View {
#StateObject // When the view creates the object, it must be a state object, or else it'll be recreated every time the view is recreated
private var viewModel = ViewModel()
var body: some View {
VStack {
Text("\(viewModel.currentNumber)") // We don't want to use functions here, as that will create a new object , as SwiftUI needs the same reference in order to keep track of changes
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published
private(set) var currentNumber: Int = 0 // Private set indicates this should only be mutated by the viewmodel
private let numberStorage = NumberStorage()
init() {
numberStorage.currentNumber
.map { $0.number }
.assign(to: &$currentNumber) // Here we're binding the current number on the storage to the published var that the view is listening to.`&$` basically assigns it to the publishers address
}
func increaseNumber() {
self.numberStorage.increaseNumber()
}
}
Model:
class NumberStorage {
private let currentNumberSubject = CurrentValueSubject<NumberObject, Never>(NumberObject())
var currentNumber: AnyPublisher<NumberObject, Never> {
currentNumberSubject.eraseToAnyPublisher()
}
func increaseNumber() {
let currentNumber = currentNumberSubject.value.number
currentNumberSubject.send(.init(number: currentNumber + 1))
}
}
struct NumberObject: Identifiable { // I'd not use this, just send and int directly
let id = UUID()
var number = 0
}
It's a known problem. Nested observable objects are not supported yet in SwiftUI. I don't think you need ViewModel+Model here since ViewModel seems to be enough.
To make this work you have to trigger objectWillChange of your viewModel manually when objectWillChange of your model is triggered:
class ViewModel: ObservableObject {
init() {
number.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: &cancellables)
}
}
You better listen to only the object you care not the whole observable class if it is not needed.
Plus:
Since instead of injecting, you initialize your viewModel in your view, you better use StateObject instead of ObservedObject. See the reference from Apple docs: Managing model data in your app
One way you could handle this is to observe the publishers in your Storage class and send the objectWillChange publisher when it changes. I have done this in personal projects by adding a class that all my view models inherit from which provides a nice interface and handles the Combine stuff like this:
Parent ViewModel
import Combine
class ViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
func publish<T>(on publisher: Published<T>.Publisher) {
publisher.sink { [weak self] _ in self?.objectWillChange.send() }
.store(in: &cancellables)
}
}
Specific ViewModel
class ContentViewModel: ViewModel {
private let numberStorage = NumberStorage()
var number: Int { numberStorage.numberObject.number }
override init() {
super.init()
publish(on: numberStorage.$numberObject)
}
func increaseNumber() {
numberStorage.increaseNumber()
}
}
View
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(viewModel.number)")
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
Model/Storage
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func increaseNumber() {
self.numberObject.number += 1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
}
This results in the view re-rendering any time Storage.numberObject changes.

How to preview Core Data data inside SwiftUI Previews

Here is a demo of what I have (kind of a lot of code, but I hope someone can follow it).
I have one entity inside Core Data named Activity with one string field. For that I use this extension to display the data in the Previews:
extension Activity {
var _name: String {
name ?? ""
}
static var example: Activity {
let controller = DataController(inMemory: true)
let viewContext = controller.container.viewContext
let activity = Activity(context: viewContext)
activity.name = "running"
return activity
}
}
For setting up Core Data I use a DataController object:
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Model")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { storeDescription, error in
if let _ = error {
fatalError("Fatal error loading store")
}
}
}
static var preview: DataController = {
let dataController = DataController(inMemory: true)
let viewContext = dataController.container.viewContext
do {
try dataController.createSampleData()
} catch {
fatalError("Fatal error creating preview")
}
return dataController
}()
func createSampleData() throws {
let viewContext = container.viewContext
for _ in 1...10 {
let activity = Activity(context: viewContext)
activity.name = "run"
}
try viewContext.save()
}
}
In the app file I do the following setup:
struct TestApp: App {
#StateObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
init() {
let dataController = DataController()
_dataController = StateObject(wrappedValue: dataController)
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
In my ContentView I display a list of this string from Core Data, which works correctly:
struct ContentView: View {
let activities: FetchRequest<Activity>
init() {
activities = FetchRequest<Activity>(entity: Activity.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Activity.name, ascending: false)], predicate: nil)
}
var body: some View {
List {
ForEach(activities.wrappedValue) { activity in
ActivityView(activity: activity)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var dataController = DataController.preview
static var previews: some View {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
.environmentObject(dataController)
}
}
But in my ActivityView where I display the string in a simple text field, previewing doesn't work.
struct ActivityView: View {
let activity: Activity
init(activity: Activity) {
self.activity = activity
}
var body: some View {
Text(activity._name)
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityView(activity: Activity.example)
}
}
I can see the string "run" in my list, 10 times, the way it is setup, but in the ActivityView screen I don't see anything displayed in the preview.
Not sure why is that, I hope someone has an idea.
edit:
I also tried this in the preview, but still doesn't work.
struct ActivityView_Previews: PreviewProvider {
static var dataController = DataController.preview
static var previews: some View {
ActivityView(activity: Activity(context: dataController.container.viewContext))
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
In SwiftUI we use the View hierarchy to convert from the rich model types to simple types. So the best way to solve this is to redesign ActivityView to work with simple types rather than the model type then it would be previewable without creating a managed object. I recommend watching Structure your app for SwiftUI previews which covers this technique and offers a few others like protocols and generics.
Btw I also noticed this problem:
init() {
let dataController = DataController()
_dataController = StateObject(wrappedValue: dataController)
}
StateObject init uses #autoclosure, e.g.
#inlinable public init(wrappedValue thunk: #autoclosure #escaping () -> ObjectType)
This means the object init needs to be inside the brackets, e.g.
_dataController = StateObject(wrappedValue: DataController())
This is what prevents the object from being init over and over again every time SwiftUI recalculates the View hierarchy.

How is possible implementing MVVM in Swift with an environment object containing the data and differents view/view models?

I'm trying to implement my app architecture loading in the app struct an object containing the data to be shared in all the views of the app through an environment object:
#main
struct SMT_testingApp: App {
#StateObject private var dataManager = DataManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataManager)
}
}
}
Here's the Datamanager class publishing the var containing the data:
class DataManager: ObservableObject {
#Published var SMTItemList: [SMTItem] = [SMTItem(id: UUID(), itemDesc: "", itemCreaDate: Date(), itemUpdDate: Date(), itemTags: [], linkedItemsUID: [])]
var urlFile: URL {
getDocumentsDirectory().appendingPathComponent("/SMT.json")
}
init() { loadData() }
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func loadData() {
//...
After here the View that contain the instance of his view model:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
List(viewModel.dataManager.SMTItemList) { item in
SMTItemView(item: item)
}
}
}
struct SMTItemView: View {
var item : SMTItem
var body: some View {
Text("Item desc: \(item.itemDesc)")
}
}
And finally, the view model that contains the environment object with the data.
extension ContentView {
class ViewModel: ObservableObject{
#EnvironmentObject var dataManager: DataManager
}
}
Now, the code is built correctly but at runtime I obtain this error in the content View:
What I'm doing wrong? Is correct implementing an architecture that way (one enviroment object with the data and many views/view models) ?
Thanks
#EnvironmentObject must be in view scope, not view model class. Like
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
#EnvironmentObject var dataManager: DataManager // << here !!
var body: some View {
List(dataManager.SMTItemList) { item in
SMTItemView(item: item)
}
}
}

How do I pass a value to a ViewModel from a View using the .environmentObject() in Swift?

I have created a ViewModel with an init() that accepts a parameter something like this. PS: Learning swift and swiftUI
//UsersViewModel.swift
class UsersViewModel: ObservableObject {
#Published var users: [User]
#Published var category: String
init(category: String) {
self.category = continentcategory
self.users = UserData().getUsers(byCategory: category)
}
}
UserData is the Data Model where I have a function getUsers(byCategory) that allows me to get a subset of data instead of all data and then filtering it.
For my SwiftUI view
//UserListByCategory.swift
import SwiftUI
struct UserListByCategory: View {
#EnvironmentObject var ud: UsersViewModel
var body: some View {
Text("Hello")
}
}
struct UserListByCategory_Previews: PreviewProvider {
static var previews: some View {
UserListByCategory()
.environmentObject(UsersViewModel(category: "Office"))
}
}
This above SwiftUI View gets called by another ListView after the user selects a category. How do I pass that category without hardcoding it here?
In SwiftUI we don't use view model objects for our view data. View structs are our primary encapsulation mechanism, e.g.
If you need to fetch your data:
struct UserListByCategory: View {
let category: String
#State var users: [User] = []
var body: some View {
List {
ForEach(users) { user in
Text("\(user.name)")
}
}
.task(id: category) {
users = await fetchUsers(category: category)
}
}
}
struct UserListByCategory_Previews: PreviewProvider {
static var previews: some View {
UserListByCategory(category: "Office")
}
}
If you already have all the model data, pass it down the View struct hierarchy as follows:
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
UserList(users: model.users(category:"Office"))
}
}
struct UserList: View {
let users: [User]
var body: some View {
List {
ForEach(users) { user in
Text("\(user.name)")
}
}
}
}

UserDefault value does not update in a list SwiftUI

I have two views, embedded in TabView.
I am using userdefaults in a class called usersettings.
class UserSettings: ObservableObject {
#Published var favList: [String] {
willSet {
print("willset")
}
didSet {
UserDefaults.standard.set(favList, forKey: "isAccountPrivate")
print("didset")
}
}
init() {
self.favList = UserDefaults.standard.object(forKey: "isAccountPrivate") as? [String] ?? ["Sepetiniz Boş"]
}
}
In Button View, which acts like add/remove favorite. It successfully adds and remove from the UserDefaults. But when I add something it does not show on the other view (please see the next code after FavButton)
struct FavButton: View {
#Binding var passedFood: String
#ObservedObject var userSettings = UserSettings()
var body: some View {
Button(action: {
if userSettings.favList.contains(passedFood) {
userSettings.favList.remove(at: userSettings.favList.firstIndex(of: passedFood )!)
} else {
userSettings.favList.append(passedFood)
}
})
}
}
But it does not update my list in this other view unless I close and open my app. If I remove something from the list, it actually removes from the userdefault.
If I add a new word within this view, it works too.
My only problem is when I add something from another view (FavButton) it does not show in this view (FavView).
struct FavView: View {
#ObservedObject var userSettings = UserSettings()
#State private var newWord = ""
var body: some View {
NavigationView {
List {
TextField("Ürün Ekleyin...", text: $newWord, onCommit: addNewWord)
ForEach( self.userSettings.favList, id: \.self) { list in
Text(list)
.font(.headline)
.padding()
}
.onDelete(perform: self.deleteRow)
}
.navigationTitle("Sepetim")
}
}
private func deleteRow(at indexSet: IndexSet) {
self.userSettings.favList.remove(atOffsets: indexSet)
}
private func addNewWord() {
let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
self.userSettings.favList.append(answer)
guard answer.count > 0 else {
return
}
newWord = ""
}
}
A better approach to follow the SwiftUI idiom is to use the .environmentObject() modifier.
When you declare your app:
struct AppScene: App {
#StateObject private var userSettings = UserSettings() // Use state object to persist the object
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userSettings) // Inject userSettings into the environment
}
}
}
and then in you ContentView you can reach into your environment and get the object:
struct ContentView: View {
#EnvironmentObject private var userSettings: UserSettings
var body: some View {
Text("Number of items in favList: \(userSettings.favList.count)")
}
}
You need to use same instance of UserSettings in all views where you want to have observed user settings, like
class UserSettings: ObservableObject {
static let global = UserSettings()
//... other code
}
and now
struct FavButton: View {
#ObservedObject var userSettings = UserSettings.global // << here !!
// ... other code
}
and
struct FavView: View {
#ObservedObject var userSettings = UserSettings.global // << here !!
// ... other code
}