How to return View for editing its property? - ios

I'm making a class that returns its editor view.
But got an error
Cannot convert value of type 'Published.Publisher' to expected
argument type 'Binding'
This is simple test code for Playgrounds.
import SwiftUI
import PlaygroundSupport
struct EditorView: View {
#Binding var text: String
var body: some View {
HStack {
Text("Name:")
TextField("Jerry", text: $text)
}
}
}
class MyModel: ObservableObject {
#Published var name: String = "Tom"
func editorView() -> some View {
EditorView(text: $name) // [ERROR] Cannot convert value of type 'Published.Publisher' to expected argument type 'Binding<String>'
}
}
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
model.editorView()
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 375, height: 400))
}
}
let viewController = UIHostingController(rootView: ContentView())
let nav = UINavigationController(rootViewController: viewController)
PlaygroundPage.current.liveView = nav
What is wrong?
2022.03.23 Added
I tested to make a view without binding, it works. I think that the class can create a view with its property. But can't pass the #Binding.
import SwiftUI
import PlaygroundSupport
struct EditorView: View {
#Binding var text: String
var body: some View {
HStack {
Text("Name:")
TextField("Jerry", text: $text)
}
}
}
// Just display the name parameter
struct DispView: View {
let name: String
var body: some View {
Text(name)
}
}
class MyModel: ObservableObject {
#Published var name: String = "Tom"
#ViewBuilder
func editorView() -> some View {
// EditorView(text: $name) // Error at this line.
DispView(name: name) // Pass the property.
}
}
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
model.editorView()
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 375, height: 400))
}
}
let viewController = UIHostingController(rootView: ContentView())
let nav = UINavigationController(rootViewController: viewController)
PlaygroundPage.current.liveView = nav

You create EditorView in wrong place, put it in your ContentView instead of your ViewModel. ViewModel is for holding data (or model) instead of View. I believe SwiftUI is not designed for carrying view along.
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
editorView
}
}
extension ContentView {
#ViewBuilder
var editorView: some View {
EditorView(text: $model.name)
}
}
If you still want to put it in your ViewModel, this should be the working code. You can create new Binding as parameter for your view initializer.
#ViewBuilder
func editorView() -> some View {
EditorView(text:
Binding { name } set: { newName in self.name = newName }
)
}

Binding is provided by StateObject wrapper (on ObservableObject), and a view should be created in body (ie. in view hierarchy), so it should look like
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
EditorView(text: model.$name) // << here !!
}
}
Note: alternate would be to make EditorView dependent to MyModel directly and then inside such view you could create a wrapper which would provide needed binding.

Related

How to properly implement a global variable in SwiftUI

I am going to create a SwiftUI application where I want to be able to swap between 3 modes. I am trying EnvironmentObject without success. I am able to change the view displayed locally, but from another View (in the end will be a class) I get a
fatal error: No ObservableObject of type DisplayView found. A View.environmentObject(_:) for DisplayView may be missing as an ancestor of this view.
Here is my code. The first line of the ContentView if/else fails.
enum ViewMode {
case Connect, Loading, ModeSelection
}
class DisplayView: ObservableObject {
#Published var displayMode: ViewMode = .Connect
}
struct ContentView: View {
#EnvironmentObject var viewMode: DisplayView
var body: some View {
VStack {
if viewMode.displayMode == .Connect {
ConnectView()
} else if viewMode.displayMode == .Loading {
LoadingView()
} else if viewMode.displayMode == .ModeSelection {
ModeSelectView()
} else {
Text("Error.")
}
TestView() //Want this to update the var & change UI.
}
.environmentObject(viewMode)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(DisplayView())
}
}
//FAILS
struct TestView: View {
#EnvironmentObject var showView: DisplayView
var body: some View {
HStack {
Button("-> load") {
self.showView.displayMode = .Loading
}
}
}
}
struct ConnectView: View {
var body: some View {
Text("Connect...")
}
}
struct LoadingView: View {
var body: some View {
Text("Loading...")
}
}
struct ModeSelectView: View {
var body: some View {
Text("Select Mode")
}
}
I would like to be able to update DisplayView from anywhere and have the ContentView UI adapt accordingly. I can update from within ContentView but I want to be able update from anywhere and have my view change.
I needed to inject BEFORE - so this fixed things up:
#main
struct fooApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DisplayView()) //super key!
}
}
}
I also tried a Singleton class to store some properties - and thus they are available from anywhere and can be updated anywhere - without having to declare EnvironmentObject. It's just another way that can work in different circumstances.
class PropContainerModel {
public var foo = "Hello"
static let shared = PropContainerModel()
private override init(){}
}
And then somewhere else
let thisFoo = PropContainerModel.shared.foo
//
PropContainerModel.shared.foo = "There"
Update here (Singleton but changes reflect in the SwiftUI UI).
class PropContainerModel: ObservableObject
{
#Published var foo: String = "Foo"
static let shared = PropContainerModel()
private init(){}
}
struct ContentView: View
{
#ObservedObject var propertyModel = PropContainerModel.shared
var body: some View {
VStack {
Text("foo = \(propertyModel.foo)")
.padding()
Button {
tapped(value: "Car")
} label: {
Image(systemName:"car")
.font(.system(size: 24))
.foregroundColor(.black)
}
Spacer()
.frame(height:20)
Button {
tapped(value: "Star")
} label: {
Image(systemName:"star")
.font(.system(size: 24))
.foregroundColor(.black)
}
}
}
func tapped(value: String)
{
PropContainerModel.shared.foo = value
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Why does SwiftUI not update a view here?

The background color and text in ReadyDashboardView below don't update when isConnected is updated. Obviously I want to update the view when the connection is made. I was expecting that publishing everything in the chain to that variable would make it update instantly in swiftui. Instead, it's always rendering using the value provided when the variable is instantiated. Here's a very simplified look at the situation:
Is there another swiftui feature i should be using or am I going to have to make some sweeping changes to my codebase?
import SwiftUI
#main
struct TestApp: App {
#StateObject private var env = PrinterEnv()
var body: some Scene {
WindowGroup {
ReadyDashboardView()
.environmentObject(env)
}
}
}
struct ReadyDashboardView: View {
#EnvironmentObject var env: PrinterEnv
var body: some View {
VStack {
HStack {
Spacer()
VStack {
Text(env.selectedPrinter?.isConnected ?? false ? "Printer Ready" : "Not Connected")
.padding(.bottom)
}
Spacer()
}
.background(env.selectedPrinter?.isConnected ?? false ? .green : .red)
Button("Connect") { env.selectedPrinter?.isConnected = true }
Button("Disconnect") { env.selectedPrinter?.isConnected = false }
}
}
}
class PrinterEnv: ObservableObject {
#Published var configuredPrinters: [Printer] = []
#Published var selectedPrinter: Printer?
init() {
configuredPrinters.append(contentsOf: [Printer()])
selectedPrinter = configuredPrinters.first
}
}
class Printer: ObservableObject {
#Published var isConnected = false
}
I suggest you do not nest ObservableObject, it does not work very well.
Try a Printer struct for example, such as:
struct Printer: Identifiable {
let id = UUID()
var isConnected = false
}
I do not think the problem is specifically related to nested observable objects. Nesting them is working fine and is even recommended by community members as the best way to manage app wide states and ensure performance is acceptable.
See this: https://www.fivestars.blog/articles/app-state/
That said, I believe it's probably related to the wrapping of the observable object with #Published property wrapper.
Try this in a playground:
import SwiftUI
class AppState: ObservableObject {
let fooState = FooState()
let barState = BarState()
}
class FooState: ObservableObject {
#Published var foo: Int = 42
}
class BarState: ObservableObject {
#Published var bar: String = Date().debugDescription
}
struct FooView: View {
#EnvironmentObject var fooState: FooState
var body: some View {
Text("foo: \(fooState.foo)")
}
}
struct BarView: View {
#EnvironmentObject var barState: BarState
var body: some View {
Text("bar: \(barState.bar)")
}
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack {
Spacer()
FooView()
.environmentObject(appState.fooState)
Button("update foo") {
appState.fooState.foo = Int.random(in: 1...100)
}
Spacer()
BarView()
.environmentObject(appState.barState)
Button("update bar") {
appState.barState.bar = Date().debugDescription
}
Spacer()
}
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(
rootView: ContentView()
.frame(width: 320, height: 414)
.environmentObject(AppState())
)

How to observe change in #StateObject in SwiftUI?

I am having property with #StateObject, I am trying to observe change in viewmodel, I am able to print correct result but can not able to show on screen as view is not refreshing.
Tried using binding but not worked because of #StateObject
import SwiftUI
struct AbcView: View {
#StateObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self._abcViewModel = StateObject(wrappedValue: abcViewModel)
}
var body: some View {
VStack(alignment: .leading) {
ZStack(alignment: .top) {
ScrollView {
Text("some txt")
}
.overlay(
VStack {
TopView(content: classViews(data: $abcViewModel.somedata, abcViewModel: abcViewModel))
Spacer()
}
)
}
}
}
}
func classViews(data: Binding<[SomeData]>, abcViewModel: AbcViewModel) -> [AnyView] {
var views: [AnyView] = []
for element in data {
views.append(
VStack(spacing: 0) {
HStack {
print("\(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? "")) )") // printing correct value
Text(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? ""))) // want to observe change here
}
}
.convertToAnyView())
}
return views
}
If you are injecting your AbcViewModel into AbcView you should use #ObserverdObject instead of #StateObject , full explanation here Also you should conform tour AbcViewModel to ObservableObject and make your desired property #Published if you want to trigger the change in View . Here is simplified code example:
Making AbcViewModel observable:
class AbcViewModel: ObservableObject {
#Published var dataID: String = "" //by changing the #Published proprty you trigger change in View using it
}
store AbcViewModel as #ObserverdObject:
struct AbcView: View {
#ObservedObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self.abcViewModel = abcViewModel
}
var body: some View {
//...
}
}
If you now use your AbcViewModel dataID property anywhere in the project, and you change its value, the property will publish the change and your View (struct) will be rebuilded. Use the same pattern for creating TopView and assigning AbcViewModel to it the same way.

Using #StateObject in iOS 14.0 while supporting iOS 13.0

I need help finding the best way to support the new #StateObject in iOS 14.0 and still supporting some alternative in iOS 13.0. Admittedly, I do not know what is the best approach in iOS 13.0. Below is what I currently have.
Does anyone have ideas on a better approach?
struct HomeView: View {
let viewModel: HomeViewModel
var body: some View {
if #available(iOS 14, *) {
HomeViewWrapper(viewModel: viewModel)
} else {
CompatibleHomeViewWrapper(viewModel: viewModel)
}
}
}
#available(iOS 14, *)
private struct HomeViewWrapper: View {
#StateObject var viewModel: HomeViewModel
var body: some View {
CompatibleHomeView(viewModel: viewModel)
}
}
private struct CompatibleHomeViewWrapper: View {
#State var viewModel: HomeViewModel
var body: some View {
CompatibleHomeView(viewModel: viewModel)
}
}
struct CompatibleHomeView: View {
#ObservedObject var viewModel: HomeViewModel
var body: some View {
Text(viewModel.someRandomName)
}
}
You can get the #StateObject behaviour by wrapping a custom propertyWrapper around #State and #ObservedObject like so:
import Combine
import PublishedObject // https://github.com/Amzd/PublishedObject
/// A property wrapper type that instantiates an observable object.
#propertyWrapper
public struct StateObject<ObjectType: ObservableObject>
where ObjectType.ObjectWillChangePublisher == ObservableObjectPublisher {
/// Wrapper that helps with initialising without actually having an ObservableObject yet
private class ObservedObjectWrapper: ObservableObject {
#PublishedObject var wrappedObject: ObjectType? = nil
init() {}
}
private var thunk: () -> ObjectType
#ObservedObject private var observedObject = ObservedObjectWrapper()
#State private var state = ObservedObjectWrapper()
public var wrappedValue: ObjectType {
if state.wrappedObject == nil {
// There is no State yet so we need to initialise the object
state.wrappedObject = thunk()
}
if observedObject.wrappedObject == nil {
// Retrieve the object from State and observe it in ObservedObject
observedObject.wrappedObject = state.wrappedObject
}
return state.wrappedObject!
}
public init(wrappedValue thunk: #autoclosure #escaping () -> ObjectType) {
self.thunk = thunk
}
}
I use this myself too so I will keep it updated at:
https://gist.github.com/Amzd/8f0d4d94fcbb6c9548e7cf0c1493eaff
Note: The most upvoted comment was that ObservedObject was very similar, which is just not true at all.
StateObject retains the object between view inits AND relays object changes to the view through willChangeObserver.
ObservedObject only relays changes to the view BUT if you create it in the view init, every time the parent view changes the object will be initialised again (losing your state).
This explanation is very crude and there is better out there please read up on it as it is an impactful part of SwiftUI.
struct StateObjectView<ViewModel: ObservableObject, Content: View>: View {
let viewModel: ViewModel
let content: () -> Content
var body: some View {
VStack {
if #available(iOS 14, *) {
StateObjectView14(viewModel: viewModel, content: content)
} else {
StateObjectView13(viewModel: viewModel, content: content)
}
}
}
}
#available(iOS 14.0, *)
struct StateObjectView14<ViewModel: ObservableObject, Content: View>: View {
#SwiftUI.StateObject var viewModel : ViewModel
let content: () -> Content
var body: some View {
content()
.environmentObject(viewModel)
}
}
struct StateObjectView13<ViewModel: ObservableObject, Content: View>: View {
#Backport.StateObject var viewModel : ViewModel
let content: () -> Content
var body: some View {
content()
.environmentObject(viewModel)
}
}
Usage:
struct ContentView: View {
#State private var reset = false
var body: some View {
VStack {
Button("reset") {
reset.toggle()
}
ScoreView()
}
}
}
class ScoreViewModel: ObservableObject {
init() {
//score = 0
print("Model Created")
}
#Published var score: Int = 0
}
struct ScoreView: View {
let viewModel : ScoreViewModel = .init()
var body: some View {
StateObjectView(viewModel: viewModel) {
ScoreContentView()
}
.onAppear {
print("ScoreView Appear")
}
}
}
struct ScoreContentView: View {
//#ObservedObject var viewModel : ScoreViewModel = .init()
#EnvironmentObject var viewModel : ScoreViewModel
#State private var niceScore = false
var body: some View {
VStack {
Button("Add Score") {
viewModel.score += 1
print("viewModel.score: \(viewModel.score)")
if viewModel.score > 3 {
niceScore = true
}
}
Text("Content Score: \(viewModel.score)")
Text("Nice? \(niceScore ? "YES" : "NO")")
}
.padding()
.background(Color.red)
}
}
Backports:
https://github.com/shaps80/SwiftUIBackports

SwiftUI Using ObservableObject in View on New Sheet

I am struggling with figuring out how to use a value assigned to a variable in an ObservableObject class in another view on another sheet. I see that it gets updated, but when I access it in the new view on the new sheet it is reset to the initialized value. How do I get it to retain the new value so I can use it in a new view on a new sheet?
ContentData.swift
import SwiftUI
import Combine
class ContentData: ObservableObject {
#Published var text: String = "Yes"
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var contentData = ContentData()
#State private var inputText: String = ""
#State private var showNewView: Bool = false
var body: some View {
VStack {
TextField("Text", text: $inputText, onCommit: {
self.assignText()
})
Button(action: {
self.showNewView = true
}) {
Text("Go To New View")
}
.sheet(isPresented: $showNewView) {
NewView(contentData: ContentData())
}
}
}
func assignText() {
print(contentData.text)
contentData.text = inputText
print(contentData.text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(contentData: ContentData())
}
}
NewView.swift
import SwiftUI
struct NewView: View {
#ObservedObject var contentData = ContentData()
var body: some View {
VStack {
Text(contentData.text)
}
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView(contentData: ContentData())
}
}
I have tried many, many different methods I have seen from other examples. I tried doing it with #EnviromentObject but could not get that to work either. I also tried a different version of the NewView.swift where I initialized the value with:
init(contentData: ContentData) {
self.contentData = contentData
self._newText = State<String>(initialValue: contentData.text)
}
I think I am close, but I do not see what I am missing. Any help would be very much appreciated. Thanks.
#ObservedObject var contentData = ContentData()
ContentData() in the above line creates a new instance of the class ContentData.
You should pass the same instance from ContentView to NewView to retain the values. Like,
.sheet(isPresented: $showNewView) {
NewView(contentData: self.contentData)
}
Stop creating new instance of ContentData in NewView and add the ability to inject ContentData from outside,
struct NewView: View {
#ObservedObject var contentData: ContentData
...
}

Resources