Mocking an EvironmentObject in PreviewProvider - swift

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

Related

SwiftUI: Model doesn't work with the TextField view

I have this simple code for my model:
import Foundation
class TaskListModel: ObservableObject
{
struct TodoItem: Identifiable
{
var id = UUID()
var title: String = ""
}
#Published var items: [TodoItem]?
//MARK: - intents
func addToList()
{
self.items!.append(TodoItem())
}
}
Then I use it in this view:
import SwiftUI
struct TasksListView: View {
#ObservedObject var model = TaskListModel()
var body: some View {
List {
Button("Add list", action: {
model.addToList()
})
ForEach(model.items!) { item in
TextField("Title", text: item.title)
}
.onMove { indexSet, offset in
model.items!.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
model.items!.remove(atOffsets: indexSet)
}
}
}
}
struct TasksListView_Previews: PreviewProvider {
static var previews: some View {
TasksListView()
}
}
I can't seem to make this code work, I suspect the items array needs to be wrapped in #Binding property wrapper, but it already wrapped in #Published, so it puzzles me even more. Any help would be appreciated!
You have forgotten to create array for items
class TaskListModel: ObservableObject
{
struct TodoItem: Identifiable
{
var id = UUID()
var title: String = ""
}
#Published var items: [TodoItem] = [] // << here !!
// ...
}
and remove everywhere force-unwrap (!!)

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

PreviewProvider and ObservedObject properties

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

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
}

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.