Working with SwiftUI views and StateObject in packages - ios

I created a SwiftUI view and ObservableObject in a package like this one:
struct CustomView: View {
#StateObject
var viewModel: CustomViewModel
var body: some View {
Text("Test")
}
}
class CustomViewModel: ObservableObject {
var folderName: String
init(folderName: String) {
self.folderName = folderName
}
}
I'm using this like here:
struct ContentViewInsidePackage: View {
var body: some View {
CustomView(viewModel: CustomViewModel(folderName: "Winter"))
}
}
How can I use the View and ObservableObject outside of the package?
The problem is when I'm using this view outside of the package then I need to declare everything public and I need a public init like here:
public struct CustomView: View {
#StateObject
var viewModel: CustomViewModel
public init(viewModel: CustomViewModel) {
// Compile Error: Cannot assign to property: 'viewModel' is a get-only property
self.viewModel = viewModel
}
public var body: some View {
Text("Test")
}
}
public class CustomViewModel: ObservableObject {
var folderName: String
public init(folderName: String) {
self.folderName = folderName
}
}
Then I get the compile error: Cannot assign to property: 'viewModel' is a get-only property
How can I instantiate the ObservableObject with the values I want to inject?

Try this:
public struct CustomView: View {
#StateObject
var viewModel: CustomViewModel
public init(viewModel: CustomViewModel) {
// Note the change on the below line:
self._viewModel = StateObject(wrappedValue: viewModel)
}
public var body: some View {
Text("Test")
}
}
public class CustomViewModel: ObservableObject {
var folderName: String
public init(folderName: String) {
self.folderName = folderName
}
}

Related

Why does associatedtype not conform to protocols defined in its constraining protocol?

I'm trying to make my SwiftUI views more "Previewable" therefore I'm making them generic over their Store (ViewModel) so I can more easily mock them.
Consider the following example:
public protocol HomeViewStore: ObservableObject {
associatedtype AnimatableImageStoreType = AnimatableImageStore
var title: String { get }
var animatableImageStores: [AnimatableImageStoreType] { get }
var buttonTapSubject: PassthroughSubject<Void, Never> { get }
var retryTapSubject: PassthroughSubject<Void, Never> { get }
}
public protocol AnimatableImageStore: ObservableObject, Identifiable {
var imageConvertible: Data? { get }
var onAppear: PassthroughSubject<Void, Never> { get }
}
struct AnimatableImage<
AnimatableImageStoreType: AnimatableImageStore
>: View {
#ObservedObject private var store: AnimatableImageStoreType
public init(store: AnimatableImageStoreType) {
self.store = store
}
...
}
public struct HomeView<
HomeViewStoreType: HomeViewStore
>: View {
#StateObject private var store: HomeViewStoreType
public init(store: HomeViewStoreType) {
self._store = StateObject(wrappedValue: store)
}
public var body: some View {
VStack {
Text(store.title)
Button {
store.buttonTapSubject.send(())
} label: {
Text("API Call")
}
.background(Color(.accent))
.padding()
Button {
store.retryTapSubject.send(())
} label: {
Text("Retry")
.font(.monserrat(.bold, 14))
}
.padding()
List(store.animatableImageStores) { animatableImageStore in
AnimatableImage(store: animatableImageStore)
}
}
.background(Color(.accent))
}
}
The code gives me the following error messages:
My questions would be, why is HomeViewStoreType.AnimatableImageStoreType not conforming to AnimatableImageStore protocol when inside HomeViewStore protocol I'm constraining it to AnimatableImageStore protocol and the same goes for Identifiable which AnimatableImageStore conforms to?
Would appreciate if someone could show me a proper way to achieve this :)
This line doesn't constraint AnimatableImageStoreType to AnimatableImageStore:
associatedtype AnimatableImageStoreType = AnimatableImageStore
Replace = by : and it will work:
associatedtype AnimatableImageStoreType: AnimatableImageStore

How to access property in viewmodel to view in SwiftUI?

I have viewmodel and view i have return some API logic in viewmodel and getting one dictionary after some logic..i want to access that dictionary value from view for ex.
viewmodel.someDic
but for now every-time i am getting empty dic.
class Viewmodel: ObservableObject {
#Published private var poductDetails:[String:ProductDetail] = [:]
func createItems(data: ProductRootClass) {
var productDetils = [String: SelectedProductDetail](){
//some logic
productDetils // with some object
self.poductDetails = productDetils
}
}
}
struct View: View {
#StateObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .leading) {
Text("\(viewModel.poductDetails)")
}
.onAppear(perform: {
print("\(viewModel.poductDetails)")
})
}
}
I want to access this dictionary from view.
I tried accessing by returning productDetils from any function but get empty everytime.
may i know the way to access property from viewmodel to view?
You need a class conforming to ObservableObject and a property marked as #Published
class ViewModel : ObservableObject {
#Published var productDetails = [String:ProductDetail]()
func createItems(data: ProductRootClass) {
var productDetils = [String: SelectedProductDetail]()
//some logic
productDetils // with some object
self.productDetails = productDetils
}
}
Whenever the property is modified the view will be updated.
In the view create an instance and declare it as #StateObject
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack(alignment: .leading) {
ForEach(viewModel.productDetails.keys.sorted(), id: \.self) { key in
Text("\(viewModel.poductDetails[key]["someOtherKey"] as! String)")
}
}
}
}
I would prefer an array as model, it's easier to access
You need to get rid of the view model and make a proper model.
class Model: ObservableObject {
#Published private var productDetails:[ProductDetail] = []
}
struct ProductDetail: Identifiable {
let id = UUID()
var title: String
}
Now you can do ForEach(model.productDetails)
You can make your viewModel as a Singleton.
class ViewModel : ObservableObject {
static let viewModelSingleton = ViewModel()
#Published var productDetails = [String : ProductDetail]()
\\API logic}
Access this viewModel Singleton in the view
struct view : View {
#ObservedObject var viewModelObject = ViewModel.viewModelSingleton
var body: some View {
VStack(alignment: .leading) {
Text("\(viewModelObject.productDetails)")
}
}}

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 view not getting updated on changing #ObservedObject

Here is a simple MVVM based TestView:
import SwiftUI
public struct Test: View {
#ObservedObject public var viewModel = TestViewModel()
public init() {
}
public var body: some View {
VStack {
Text(viewModel.model.stri)
Button(action: {
self.viewModel.change()
}) {
Text("Change")
}
}.padding(50)
}
}
public class TestModel {
#Published public var condition: Bool = false
#Published var stri = "Random numbers"
}
public class TestViewModel: ObservableObject {
#Published var model = TestModel()
func change() {
model.condition.toggle()
model.stri = "\(Int.random(in: 1...10))"
}
}
The view does not get updated when the model is updated from inside the view model.
The text should finally produce some random number between 1 and 10. Please let me know where I am going wrong.
It is because your Test view observes viewModel but not viewModel.model which is not changed in your scenario because it is a reference-type
The following is a solution
func change() {
model.condition.toggle()
model.stri = "\(Int.random(in: 1...10))"
self.objectWillChange.send() // << this one !!
}

View refreshing not triggered when ObservableObject is inherited in SwiftUI

ContentView2 view is not refreshed when model.value changes, if Model conforms to ObservableObject directly instead of inheriting SuperModel then it works fine
class SuperModel: ObservableObject {
}
class Model: SuperModel {
#Published var value = ""
}
struct ContentView2: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Text(model.value)
Button("change value") {
self.model.value = "\(Int.random(in: 1...10))"
}
}
}
}
Here is working variant of your example. See that to be able to work, not only chaining the publishers is required, but at least one Published property. So or so, it could help in some scenario.
import SwiftUI
class SuperModel: ObservableObject {
// this is workaround but not real trouble.
// without any value in supermodel there is no real usage of SuperModel at all
#Published var superFlag = false
}
class Model: SuperModel {
#Published var value = ""
override init() {
super.init()
_ = self.objectWillChange.append(super.objectWillChange)
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Text(model.value)
Button("change value") {
self.model.value = "\(Int.random(in: 1...10))"
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
changing the code to
var body: some View {
VStack {
Text(model.value)
Button("change value") {
self.model.value = "\(Int.random(in: 1...10))"
}
Text(model.superFlag.description)
Button("change super flag") {
self.model.superFlag.toggle()
}
}
}
you can see how to use even your supermodel at the same time
Use ObjectWillChange to solve the problem specified.
Here is the working code:
import SwiftUI
class SuperModel: ObservableObject {
}
class Model: SuperModel {
var value: String = "" {
willSet { self.objectWillChange.send() }
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Text("Model Value1: \(model.value)")
Button("change value") {
self.model.value = "\(Int.random(in: 1...10))"
}
Text("Model Value2: \(model.value)")
}
}
}
This really looks like heavy defect.
class SuperModel: ObservableObject {
}
class Model: SuperModel {
#Published var value = ""
}
as I see the value is changed and keep new one as expected, but DynamicProperty feature does not work
The following variant works for me (Xcode 11.2 / iOS 13.2)
class SuperModel: ObservableObject {
#Published private var stub = "" // << required !!!
}
class Model: SuperModel {
#Published var value = "" {
willSet { self.objectWillChange.send() } // < works only if above
}
}
Also such case is possible for consideration:
class SuperModel {
}
class Model: SuperModel, ObservableObject {
#Published var value = ""
}

Resources