SwiftUI - PreviewProvider with private var - swiftui

I'm using CoreData to populate fields in a view that will get converted to a PDF. It would be great to have the preview feature working to help design the view, however, I've run into a snag with a private variable. Specifically, I'm getting an error that says, "initializer is inaccessible due to 'private' protection level". As you can see below, company is the private variable. I can preview made up data for the quote variable using the code in the preview section, however, the same does not work for company.
struct PDFQuoteView: View {
#Environment(\.managedObjectContext) var moc
#State private var company: Company
var quote: Quote?
var body: some View {
struct PDFQuoteView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let newQuote = Quote.init(context: context)
let newCompany = Company.init(context: context)
newQuote.quoteNumber = "AA0202"
newCompany.name = "Acme Incorporated"
return PDFQuoteView(company: newCompany, quote: newQuote).environment(\.managedObjectContext, context)
.previewLayout(.fixed(width: 612, height: 792)) //this is the error line
}
}

For the view as you have this error can be fixed by adding explicit init, like
struct PDFQuoteView: View {
#Environment(\.managedObjectContext) var moc
#State private var company: Company
private var quote: Quote?
init(company: Company, quote: Quote?) {
self._company = State(initialValue: company)
self.quote = quote
}
var body: some View {
...

Don't even need the init, you could just give your state a default value:
#State private var company: Company = Company.company
And you don't even have to declare the Type, it can be inferred:
#State private var company = Company.company
If you don't want to give your state and initial value it can be nil so long as your UI handles nil.
i.e. You should already be using a default: in switch statements.
But you could also just pass the state as a constant in the preview:
PDFQuoteView(company: .constant(Company.company))
I also noticed the posted sample is missing the closing bracket } for your Struct counting the body and PreviewProvider Struct. You may have not copied it, or there may be more to the end of your Struct, but make sure it's actually there if this was the entire Struct.

Related

SwiftUI pass Binding by ref to a child ViewModel

In SwiftUI, I am trying to create some binding between a parent ViewModel and a child ViewModel, here is a simplified example of my scenario:
The parent component:
class ParentViewModel : ObservableObject {
#Published var name = "John Doe"
func updateName() {
self.name = "Jonnie Deer"
}
}
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
ChildView(name: $viewModel.name)
// tapping the button the text on parent view is updated but not in child view
Button("Update", action: viewModel.updateName)
}
}
}
The child component:
class ChildViewModel : ObservableObject {
var name: Binding<String>
var displayName: String {
get {
return "Hello " + name.wrappedValue
}
}
init(name: Binding<String>) {
self.name = name
}
}
struct ChildView: View {
#StateObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
init(name: Binding<String>) {
_viewModel = StateObject(wrappedValue: ChildViewModel(name: name))
}
}
So, as stated in the comments, when I tap the button on the parent component the name is not getting updated in ChildView, as if the binding is lost...
Is there any other way to update view model with the updated value? say something like getDerivedStateFromProps in React (becuase when tapping the button the ChildView::init method is called with the new name.
Thanks.
Apple is very big on the concept of a Single Source of Truth(SSoT), and keeping it in mind will keep you from getting into the weeds in code like this. The problem you are having is that while you are using a Binding to instantiate the child view, you are turning around and using it as a #StateObject. When you do that, you are breaking the connection as #StateObject is supposed to sit at the top of the SSoT hierarchy. It designates your SSoT. Otherwise, you have two SSoTs, so you can only update one. The view model in ChildView should be an #ObservedObject so that it connects back up the hierarchy. Also, you can directly instantiate the ChildViewModel when you call ChildView. The initializer just serves to decouple things. Your views would look like this:
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
// You can directly use the ChildViewModel to instantiate the ChildView
ChildView(viewModel: ChildViewModel(name: $viewModel.name))
Button("Update", action: viewModel.updateName)
}
}
}
struct ChildView: View {
// Make this an #ObservedObject not a #StateObject
#ObservedObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
}
Neither view model is changed.
Get rid of the view model objects and do #State var name = “John” in ParentView and #Binding var name: String in ChildView. And pass $name into ChildView’s init which gives you write access as if ParentView was a view model object.
By using #State and #Binding you get the reference type semantics you want inside a value type which is the power of SwiftUI. If you just use objects you lose that benefit and have more work to do.
We usually only use ObservableObject for model data but we can also use it for loaders/fetchers where we want to tie some controller behaviour to the view lifecycle but for data transient to a view we always use #State and #Binding. You can extract related vars into their own struct and use mutating funcs for other logic and thus have a single #State struct used by body instead of multiple. This way it can still be testable like a view model object in UIKit would be.

SwiftUI clean up ContentView

I'm trying to simplify the ContentView within a project and I'm struggling to understand how to move #State based logic into its own file and have ContentView adapt to any changes. Currently I have dynamic views that display themselves based on #Binding actions which I'm passing the $binding down the view hierarchy to have buttons toggle the bool values.
Here's my current attempt. I'm not sure how in SwiftUI to change the view state of SheetPresenter from a nested view without passing the $binding all the way down the view stack. Ideally I'd like it to look like ContentView.overlay(sheetPresenter($isOpen, $present).
Also, I'm learning SwiftUI so if this isn't the best approach please provide guidance.
class SheetPresenter: ObservableObject {
#Published var present: Present = .none
#State var isOpen: Bool = false
enum Present {
case none, login, register
}
#ViewBuilder
func makeView(with presenter: Present) -> some View {
switch presenter {
case .none:
EmptyView()
case .login:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
LoginScreen()
}
case .register:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
RegisterScreen()
}
}
}
}
if you don't want to pass $binding all the way down the view you can create a StateObject variable in the top view and pass it with .environmentObject(). and access it from any view with EnvironmentObject
struct testApp: App {
#StateObject var s1: sViewModel = sViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(s1)
}
}
}
You are correct this is not the best approach, however it is a common mistake. In SwiftUI we actually use #State for transient data owned by the view. This means using a value type like a struct, not classes. This is explained at 4:18 in Data Essentials in SwiftUI from WWDC 2020.
EditorConfig can maintain invariants on its properties and be tested
independently. And because EditorConfig is a value type, any change to
a property of EditorConfig, like its progress, is visible as a change
to EditorConfig itself.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
mutating func present(initialProgress: Double) {
progress = initialProgress
note = ""
isEditorPresented = true
}
}
struct BookView: View {
#State private var editorConfig = EditorConfig()
func presentEditor() { editorConfig.present(…) }
var body: some View {
…
Button(action: presentEditor) { … }
…
}
}
Then you just use $editorConfig.isEditorPresented as the boolean binding in .sheet or .overlay.
Worth also taking a look at sheet(item:onDismiss:content:) which makes it much simpler to show an item because no boolean is required it uses an optional #State which you can set to nil to dismiss.

SwiftUI Preview issue with #State and CoreData

I'm having trouble getting a preview to work with what seems like a pretty simple struct. Customer is a CoreData entity:
struct CustomerDetailView: View {
#Environment(\.managedObjectContext) var moc
#State var showNewCustomer = false
var customer: Customer
var body: some View {
I've tried almost everything that doesn't work, including this:
struct CustomerDetail_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return
CustomerDetailView(customer: --Not sure what works here-- ).environment(\.managedObjectContext, context)
}
}
I've tried static let customer = Customer() so that I would have a customer variable to use in the last line, but that did not help.
Preview uses different container for apps, so you can just create new Customer, like
struct CustomerDetail_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return
CustomerDetailView(customer: Customer(context: context))
.environment(\.managedObjectContext, context)
}
}

How could I initialize the #State variable in the init function in SwiftUI?

Let's see the simple source code:
import SwiftUI
struct MyView: View {
#State var mapState: Int
init(inputMapState: Int)
{
mapState = inputMapState //Error: 'self' used before all stored properties are initialized
} //Error: Return from initializer without initializing all stored properties
var body: some View {
Text("Hello World!")
}
}
I need the init function here because I want to do some data loading here, but there is one problem, the #State variable could not be initialize here! How could I do with that?
Maybe it's a very simple question, but I don't know how to do.
Thanks very much!
Property wrappers are generating some code for you. What you need to know is the actual generated stored property is of the type of the wrapper, hence you need to use its constructors, and it is prefixed with a _. In your case this means var _mapState: State<Int>, so following your example:
import SwiftUI
struct MyView: View {
#State var mapState: Int
init(inputMapState: Int)
{
_mapState = /*State<Int>*/.init(initialValue: inputMapState)
}
var body: some View {
Text("Hello World!")
}
}
We can inject the data that the view needs.
Used a model which has access to the data that you wanted. Make a map view and use that instance of it in your parent view. This will also help to unit test the model.
Used the property wrapper #Binding to pass the data from the parent view to MapView and used _mapState which holds the value of mapState.
struct Model {
//some data
}
struct MapView {
private let model: Model
#Binding var mapState: Int
init(model: Model, mapState: Binding<Int>) {
self.model = model
self._mapState = mapState
}
}
extension MapView: View {
var body: some View {
Text("Map Data")
}
}
I think that it would better to initialize when you write the code, just like:
#State var mapState = 0
or, if you want to binding the value with another view, use #Binding.
You have more information at https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-binding-property-wrapper

SwiftUI - Updating #State when Global changes

I'd like to update an UI element on an overview view when data on another view is changed.
I looked into #EnvironmentalObject and #Binding. However, an update to either object does not appear to force a view reload. Only changes to #State force renders.
Also, in the case described below, the ChangeView is not a child of OverviewView. Therefore #Binding is not an option anyway.
Data.swift
struct ExampleData : Hashable {
var id: UUID
var name: String
}
var globalVar: ExampleData = ExampleData(id: UUID(), name:"")
OverviewView.swift
struct OverviewView: View {
#State private var data: ExampleData = globalVar
var body: some View {
Text(data.name)
}
}
ChangeView.swift
struct ChangeView: View {
#State private var data: ExampleData = globalVar
var body: some View {
TextField("Name", text: $data.name, onEditingChanged: { _ in
globalVar = data }, onCommit: { globalVar = data })
}
}
Changes within the ChangeView TextField will update the globalVar. However, this will not update the Text on the OverviewView when switching back to the view.
I am aware that using global variables is "ugly" coding. How do I handle data that will be used in a multitude of unrelated views?
Please advise on how to better handle such a situation.
OverviewView and ChangeView hold different copies of the ExampleData struct in their data variables (When assigning a struct to another variable, you're effectively copying it instead of referencing it like an object.) so changing one won't affect the other.
#EnvironmentObject suits your requirements.
Here's an example:
Since, we're using #EnvironmentObject, you need to either convert ExampleData to
a class, or use a class to store it. I'll use the latter.
class ExampleDataHolder: ObservableObject {
#Published var data: ExampleData = ExampleData(id: UUID(), name:"")
}
struct CommonAncestorOfTheViews: View {
var body: some View {
CommonAncestorView()
.environmentObject(ExampleDataHolder())
}
}
struct OverviewView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
Text(dataHolder.data.name)
}
}
struct ChangeView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
TextField("Name", text: $dataHolder.data.name)
}
}