structs and classes in ContentView_Previews - xcode

I have a struct:
struct Item: Identifiable {
let id = UUID()
var isComplete: Bool = false
}
Also let item: Item
And a class:
class Model: ObservableObject {
#Published var isOn: Bool = false
#Published var arr = [Item(isComplete: true), Item(isComplete: false), Item(isComplete: true), Item(isComplete: false), Item(isComplete: true), Item(isComplete: true)]
}
And #ObservedObject var model: Model
How to properly put item and model into ContentView_Previews?
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(
item: <#Item#>,
model: <#Model#>
)
}
}
I don't know what should I do next :/

It would be nice to add code for the ContentView. But if it looks like this:
struct ContentView: View {
var item: Item
#EnvironmentObject var model: Model
var body: some View {
// no matter what here
}
}
you can write this:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(item: Item(isComplete: false))
.environmentObject(Model())
}
}

Related

Populating SwiftUI List with array elements that can be editied in TextEditor

I have a SwiftUI app that produces a List made from elements of an array of columns held in a struct.
I need the items in the row to be editable so I'm trying to use TextEditor but the bindings are proving difficult. I have a working prototype however the TextEditors are uneditable - I get the warning:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
Here's a much shortened version of my code which produces the same problem:
import SwiftUI
struct Item: Identifiable {
#State var stringValue: String
var id: UUID = UUID()
}
struct ArrayContainer {
var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
}
struct ContentView: View {
#State var wrapperArray: ArrayContainer = ArrayContainer()
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach (Array(wrapperArray.items.enumerated()), id: \.offset) { index, item in
TextEditor(text: item.$stringValue)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
How can I bind the TextEditor to the items stringValues within the items array?
TIA.
#State should only be used as a property wrapper on your View -- not on your model.
You can use a binding within ForEach using the $ syntax to get an editable version of the item.
struct Item: Identifiable {
var stringValue: String
var id: UUID = UUID()
}
struct ArrayContainer {
var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
}
struct ContentView: View {
#State var wrapperArray: ArrayContainer = ArrayContainer()
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach ($wrapperArray.items, id: \.id) { $item in
TextEditor(text: $item.stringValue)
}
}
}
}
}
This could be simplified further to avoid the ArrayContainer if you want:
struct ContentView: View {
#State var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach ($items, id: \.id) { $item in
TextEditor(text: $item.stringValue)
}
}
}
}
}

How do I store the Input of a Textfield and display it in another View in Swift UI?

I am just learning to code and I have a question. How do I store the Input data of a Textfield and display it in another View? I tried it with Binding but it doesn't work that way. I appreciate your help
import SwiftUI
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
struct DisplayUserName: View {
#State private var name = ""
var body: some View {
// the name should be diplayed here!
Text(name)
}
}
struct DisplayUserName_Previews: PreviewProvider {
static var previews: some View {
DisplayUserName()
}
}
State should always be stored in a parent and passed down to the children. Right now, you're not showing the connection between the two views (neither reference the other), so it's a little unclear how they relate, but there are basically two scenarios:
Your current code would work if DisplayUserName was the parent of SelectUserName:
struct DisplayUserName: View {
#State private var name = ""
var body: some View {
Text(name)
SelectUserName(name: $name)
}
}
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
Or, if they are sibling views, the state should be stored by a common parent:
struct ContentView : View {
#State private var name = ""
var body: some View {
SelectUserName(name: $name)
DisplayUserName(name: name)
}
}
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
struct DisplayUserName: View {
var name : String //<-- Note that #State isn't needed here because nothing in this view modifies the value
var body: some View {
Text(name)
}
}

Why doesn't this binding work when it's in a subview on macOS?

I have a couple layers of ObservableObjects and Published properties. When I use them directly in a view, they seem to work as expected. However, when I try to move the list into it's own type, the bindings in the parent view don't seem to work.
For example, why the ModelList is enabled, when you select rows, the Button does not toggle between enabled and disabled. However, if you comment that out and enable the List.init lines, then when selecting and unselecting rows, the Button correctly enables and disables.
https://youtu.be/7Kvh2w8z__4
This works
View
List(selection: viewModel.dataStore.selection)
This does not
View
ModelList(dataStore: viewModel.dataStore)
List(selection: dataStore.selection)
Full code example
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
// Using the the dataStore, the button bind works
// List.init(viewModel.dataStore.models, id: \.id, selection: $viewModel.dataStore.selection) {
// Text("Name: \($0.name)")
// }
// Using the dataStore in the subview, the button binding doesn't work
ModelList(dataStore: viewModel.dataStore)
Button(action: {
print("Delete")
}, label: {
Image(systemName: "minus")
})
.disabled($viewModel.dataStore.selection.wrappedValue.count == 0)
Text("Selection \($viewModel.dataStore.selection.wrappedValue.count)")
}
}
}
struct ModelList: View {
#ObservedObject public var dataStore: DataStore
var body: some View {
List.init(dataStore.models, id: \.id, selection: $dataStore.selection) {
Text("Name: \($0.name)")
}
}
}
class ViewModel: ObservableObject {
#Published var dataStore: DataStore = DataStore()
}
class DataStore: ObservableObject {
#Published public var selection = Set<Int>()
#Published public var models = [Model(id: 1, name: "First")]
}
struct Model: Identifiable {
let id: Int
let name: String
}
#main
struct LayersApp: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: ViewModel())
}
}
}
The subview should accept a Binding, not another ObservedObject.
#ObservedObject public var dataStore: DataStore should be #Binding public var dataStore: DataStore
Now when using the subview, pass in the binding ModelList(dataStore: $viewModel.dataStore)
Complete working example:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
ModelList(dataStore: $viewModel.dataStore)
Button(action: {
print("Delete \(viewModel.dataStore.selection)")
}, label: {
Image(systemName: "minus")
})
.disabled($viewModel.dataStore.selection.wrappedValue.count == 0)
Text("Selection \($viewModel.dataStore.selection.wrappedValue.count)")
}
}
}
struct ModelList: View {
#Binding public var dataStore: DataStore
var body: some View {
List.init(dataStore.models,
id: \.id,
selection: $dataStore.selection) {
Text("Name: \($0.name)")
}
}
}
class ViewModel: ObservableObject {
#Published var dataStore: DataStore = DataStore()
init() {
print("ViewModel")
}
}
class DataStore: ObservableObject {
#Published public var selection = Set<Int>()
#Published public var models = [Model(id: 1, name: "First")]
init() {
print("DataStore")
}
}
struct Model: Identifiable, Equatable {
let id: Int
let name: String
}
#main
struct LayersApp: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: ViewModel())
}
}
}

How to create Generic if #EnvironmentObject?

I've recently come across the need to write a Mock of a Class, as it causes the SwiftUI preview from working. Unfortunately, I get the error:
Property type 'T' does not match that of the 'wrappedValue' property of its wrapper type 'EnvironmentObject'
In the View struct:
struct ContentView<T>: View {
#EnvironmentObject var mockFoobar: T
...
}
And also the error:
Type of expression is ambiguous without more context
For the Preview struct:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let mockFoobar: MockFoobar = MockFoobar()
return ContentView<MockFoobar>()
.environmentObject(mockFoobar)
}
}
The MockFoobar class is:
class MockFoobar: ObservableObject {
...
}
As the user #Asperi kindly provided, tested the following as suggested:
class Foobar: ObservableObject {
#Published var param: Bool = false
func start() {
self.param = true
}
}
struct MyFoobarView<T: ObservableObject>: View {
#EnvironmentObject var foobar: T
var body: some View {
VStack {
Text("Hello Foobar")
}
.onAppear {
self.foobar.start()
}
}
}
struct MyFoobarView_Previews: PreviewProvider {
static var previews: some View {
let foobar: Foobar = Foobar()
return MyFoobarView()
.environmentObject(foobar)
}
}
But I get the following errors (the first in the .onAppear and the second in the PreviewProvider):
Cannot call value of non-function type 'Binding<Subject>'
Generic parameter 'T' could not be inferred
The EnvironmentObject must be ObservableObject, so here is fix
struct ContentView<T: ObservableObject>: View {
#EnvironmentObject var mockFoobar: T
// .. other code here
Update: added demo with introduced model protocol
protocol Foobaring {
var param: Bool { get set }
func start()
}
class Foobar: ObservableObject, Foobaring {
#Published var param: Bool = false
func start() {
self.param = true
}
}
struct MyFoobarView<T: ObservableObject & Foobaring>: View {
#EnvironmentObject var foobar: T
var body: some View {
VStack {
Text("Hello Foobar")
}
.onAppear {
self.foobar.start()
}
}
}
struct MyFoobarView_Previews: PreviewProvider {
static var previews: some View {
let foobar: Foobar = Foobar()
return MyFoobarView<Foobar>()
.environmentObject(foobar)
}
}

Updating SwiftUI View based on Observable in Preview

Trying to implement a Login screen in SwiftUI. Based on other similar questions, I'm going the approach of using an Observable EnvironmentObject and a ViewBuilder in the main ContentView that reacts to that and displays the appropriate screen.
However, even though the property is updating as expecting the view never changes in Preview. Everything works fine when built and run in the Simulator but in Preview the change never happens.
Below is the code reduced to the smallest possible example in a single file (only missing passing the environment object in SceneDelegate, which doesn't affect Preview anyway).
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
#ViewBuilder
var body: some View {
if !userAuth.person.isLoggedin {
FirstView()
} else {
SecondView()
} }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
struct Person {
var isLoggedin: Bool
init() {
self.isLoggedin = false
}
}
class UserAuth: ObservableObject {
#Published var person: Person
init(){
self.person = Person()
}
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
willChange.send(self)
self.person.isLoggedin = true
didChange.send(self)
}
}
struct SecondView: View {
var body: some View {
Text("Second View!")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(UserAuth())
}
}
struct FirstView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
VStack {
Button(action: {
self.userAuth.login()
}) {
Text("Login")
}
Text("Logged in: " + String(self.userAuth.person.isLoggedin))
}
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView().environmentObject(UserAuth())
}
}
EDIT: Based on the answer below, I've added the environment object to the interior views, but unfortunately the view still doesn't change in Preview mode.
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView().environmentObject(UserAuth())
}
}
environment object must be set in PreviewProvider as well
UPDATE
struct ContentView: View {
#ObservedObject var userAuth = UserAuth () // #ObservedObject
var body: some View {
NavigationView{ // Navigation
}.environmentObject(UserAuth) //.environmentObject
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
struct SecondView: View {
#EnvironmentObject var userAuth: UserAuth // only EnvironmentObject
var body: some View {
Text("Second View!")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(UserAuth())
}
}
The issue I was having with Canvas not giving previews is that my ObservableObject was reading from User Defaults
#Published var fName: String = Foundation.UserDefaults.standard.string(forKey: "fName") !
{ didSet {
Foundation.UserDefaults.standard.set(self.fName, forKey: "fName")
}
}
So works in simulator and on device but no Canvas Previews. I tried many ways to give Preview data to use since Preview can't read from UserDefaults (not a device), and realized I can put an initial/ default value if the UserDefault is not there:
#Published var fName: String = Foundation.UserDefaults.standard.string(forKey: "fName") ?? "Sean"
{ didSet {
Foundation.UserDefaults.standard.set(self.fName, forKey: "fName")
}
}
Now Preview/ Canvas is showing my view and I can continue coding with my Observable Object. The aim was to put in the Observable Object some default code to use.

Resources