PreviewProvider and ObservedObject properties - swift

how can I set a property to my CoreData Object which has the type CDObject, it has a property called name: String
My issue is now that I do not know how to set the name property in the PreviewProvider
Here is the code:
struct MainView: View {
#ObservedObject var obj: CDObject
var body: some View {
Text("Hello, World!")
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(obj: CDObject())
}
}
I would like to do something like, before passing it to the View:
let itm = CDObject()
itm.name = "Hello"

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

Related

typecasting with if let 'someClass' as? SomeClass and binding to 'someClass.'someProp' does not work

I have the following construct
import SwiftUI
class TopClass: ObservableObject {
#Published var someArr = [SomeClass(), SomeSubclass()]
}
class SomeClass: Identifiable {
let id = UUID()
}
class SomeSubclass: SomeClass {
var toggle = true
}
struct ContentView: View {
#ObservedObject var topClass = TopClass()
var body: some View {
ForEach($topClass.someArr) {
$value in
if let someSubclass = $value as? SomeSubclass {
Toggle("Test", isOn: someSubclass.toggle)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I can`t figure out if it is possible to somehow bind someSubclass.toggle to isOn of the Toggle. I solution or a hint to where I could research this would be very appreciated.
If you are really hell-bent on using this approach, then try this example code:
struct ContentView: View {
#ObservedObject var topClass = TopClass()
var body: some View {
ForEach($topClass.someArr) { $value in
if let someSubclass = $value.wrappedValue as? SomeSubclass {
Toggle("test", isOn: Binding<Bool> (
get: { someSubclass.toggle },
set: {
topClass.objectWillChange.send()
someSubclass.toggle = $0
})
)
}
}
}
}

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.

Unable to infer complex closure return type swiftUI

I'm trying to post data to the list but keep getting the error 'Unable to infer complex closure return type; add explicit type to disambiguate'
how do I fix this?
import SwiftUI
struct ContentView: View {
#State var data: [Post] = [Post]()
#ObservedObject var networkManager = NetworkManager()
#State private var searchTerm: String = "" {
didSet {
print(searchTerm)
}
}
var body: some View {
List { // ERROR SHOWS UP HERE
SearchBar(text: $searchTerm)
ForEach(data) { post in
Text(post.fullname ?? "null")
}
}
.onAppear {
self.reload()
}
.onReceive(self.networkManager.posts, perform: { _ in
self.reload()
})
}
private func reload() {
networkManager.fetchData(playerName: "messi")
self.data = networkManager.posts
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Assuming your NetworkManage.posts is #Published property the subscriber in view have to be specified as follow
.onReceive(self.networkManager.$posts, perform: {_ in // << fixed !!
self.reload()
})
Note: btw, didSet does not work for #State, so don't spend time on that.

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

Mocking an EvironmentObject in PreviewProvider

I'm playing around with SwiftUI using an EnvironmentObject for my data source. I'm wondering how I can mock this when using the PreviewProvider.
Example code below:
struct ListView: View {
#State private var query: String = "Swift"
#EnvironmentObject var listData: ListData
var body: some View {
NavigationView {
List(listData.items) { item in
ListItemCell(item: item)
}
}.onAppear(perform: fetch)
}
private func fetch() {
listData.fetch()
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
How do I mock this?
// ListView(listData: EnvironmentObject<ListData>)
}
}
class ListData: BindableObject {
var items: [ListItem] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ListData, Never>()
func fetch() {
// async call that updates my items
self?.items = someNetworkResponse
}
}
This worked fine, in my ListData class:
#if DEBUG
let mockedListView = ListView().environmentObject(ListData())
#endif