I have a base API:
class API: ObservableObject {
#Published private(set) var isAccessTokenValid = false
#AppStorage("AccessToken") var accessToken: String = ""
#AppStorage("RefreshToken") var refreshToken: String = ""
func request1() {}
func request2() {}
}
And it was passed to all views by using .environmentObject(API()). So in any views can easily access the API to do the http request calls.
Also I have a view model to fetch some data on the view appears:
class ViewModel: ObservableObject {
#Published var data: [SomeResponseType]
init() {
// do the request and then init data using the response
}
}
struct ViewA: View {
#StateObject private var model = ViewModel()
var body: View {
VStack {
model.data...
}
}
}
But in the init(), the API is not accessable in the ViewModel.
So, to solve this problem, I found 3 solutions:
Solution 1: Change API to Singleton:
class API: ObservableObject {
static let shared = API()
...
}
Also we should change the enviromentObject(API()) to enviromentObject(API.shared).
So in the ViewModel, it can use API.shared directly.
Solution 2: Call the request on the onAppear/task
struct ViewA: View {
#EnvironmentObject var api: API
#State private var data: [SomeResponseType] = []
var body: View {
VStack {}
.task {
let r = try? await api.request1()
if let d = r {
data = d
}
}
}
}
Solution 3: Setup the API to the ViewModel onAppear/task
class ViewModel: ObservableObject {
#Published var data: [SomeResponseType]
var api: API?
setup(api: API) { self.api = api }
requestCall() { self.api?.reqeust1() }
}
struct ViewA: View {
#EnvironmentObject var api: API
#StateObject private var model = ViewModel()
var body: View {
VStack {}
.onAppear {
model.setup(api)
model.requestCall()
}
}
}
Even though, I still think they are not a SwiftUI way. And my questions is a little XY problem. Probably, the root question is how to refactor my API. But I am new to SwiftUI.
Solution 2 is best. You can also try/catch the exception and set an #State for an error message.
Try to avoid using UIKit style view model objects because in SwiftUI the View data struct plus #State already fulfils that role. You only need #StateObject when you need a reference type for view data which is not very often given now we have .task.
Related
How to observe property value in SwiftUI.
I know some basic publisher and observer patterns. But here is a scenario i am not able to implement.
class ScanedDevice: NSObject, Identifiable {
//some variables
var currentStatusText: String = "Pending"
}
here CurrentStatusText is changed by some other callback method that update the status.
Here there is Model class i am using
class SampleModel: ObservableObject{
#Published var devicesToUpdated : [ScanedDevice] = []
}
swiftui component:
struct ReviewView: View {
#ObservedObject var model: SampleModel
var body: some View {
ForEach(model.devicesToUpdated){ device in
Text(device.currentStatusText)
}
}
}
Here in UI I want to see the real-time status
I tried using publisher inside ScanDevice class but sure can to use it in 2 layer
You can observe your class ScanedDevice, however you need to manually use a objectWillChange.send(),
to action the observable change, as shown in this example code.
class ScanedDevice: NSObject, Identifiable {
var name: String = "some name"
var currentStatusText: String = "Pending"
init(name: String) {
self.name = name
}
}
class SampleViewModel: ObservableObject{
#Published var devicesToUpdated: [ScanedDevice] = []
}
struct ReviewView: View {
#ObservedObject var viewmodel: SampleViewModel
var body: some View {
VStack (spacing: 33) {
ForEach(viewmodel.devicesToUpdated){ device in
HStack {
Text(device.name)
Text(device.currentStatusText).foregroundColor(.red)
}
Button("Change \(device.name)") {
viewmodel.objectWillChange.send() // <--- here
device.currentStatusText = UUID().uuidString
}.buttonStyle(.bordered)
}
}
}
}
struct ContentView: View {
#StateObject var viewmodel = SampleViewModel()
var body: some View {
ReviewView(viewmodel: viewmodel)
.onAppear {
viewmodel.devicesToUpdated = [ScanedDevice(name: "device-1"), ScanedDevice(name: "device-2")]
}
}
}
I have a question regarding the combination of SwiftUI and MVVM.
Before we start, I have read some posts discussing whether the combination of SwiftUI and MVVM is necessary. But I don't want to discuss this here, as it has been covered elsewhere. I just want to know if it is possible and, if yes, how. :)
So here comes the code. I tried to add the ViewModel Layer in between the updated Object class that contains a number that should be updated when a button is pressed. The problem is that as soon as I put the ViewModel Layer in between, the UI does not automatically update when the button is pressed.
View:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#ObservedObject var numberStorage = NumberStorage()
var body: some View {
VStack {
// Text("\(viewModel.getNumberObject().number)")
// .padding()
// Button("IncreaseNumber") {
// viewModel.increaseNumber()
// }
Text("\(numberStorage.getNumberObject().number)")
.padding()
Button("IncreaseNumber") {
numberStorage.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published var number: NumberStorage
init() {
self.number = NumberStorage()
}
func increaseNumber() {
self.number.increaseNumber()
}
func getNumberObject() -> NumberObject {
self.number.getNumberObject()
}
}
Model:
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func getNumberObject() -> NumberObject {
return self.numberObject
}
public func increaseNumber() {
self.numberObject.number+=1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
} ```
Looking forward to your feedback!
I think your code is breaking MVVM, as you're exposing to the view a storage model. In MVVM, your ViewModel should hold only two things:
Values that your view should display. These values should be automatically updated using a binding system (in your case, Combine)
Events that the view may produce (in your case, a button tap)
Having that in mind, your ViewModel should wrap, adapt and encapsulate your model. We don't want model changes to affect the view. This is a clean approach that does that:
View:
struct ContentView: View {
#StateObject // When the view creates the object, it must be a state object, or else it'll be recreated every time the view is recreated
private var viewModel = ViewModel()
var body: some View {
VStack {
Text("\(viewModel.currentNumber)") // We don't want to use functions here, as that will create a new object , as SwiftUI needs the same reference in order to keep track of changes
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published
private(set) var currentNumber: Int = 0 // Private set indicates this should only be mutated by the viewmodel
private let numberStorage = NumberStorage()
init() {
numberStorage.currentNumber
.map { $0.number }
.assign(to: &$currentNumber) // Here we're binding the current number on the storage to the published var that the view is listening to.`&$` basically assigns it to the publishers address
}
func increaseNumber() {
self.numberStorage.increaseNumber()
}
}
Model:
class NumberStorage {
private let currentNumberSubject = CurrentValueSubject<NumberObject, Never>(NumberObject())
var currentNumber: AnyPublisher<NumberObject, Never> {
currentNumberSubject.eraseToAnyPublisher()
}
func increaseNumber() {
let currentNumber = currentNumberSubject.value.number
currentNumberSubject.send(.init(number: currentNumber + 1))
}
}
struct NumberObject: Identifiable { // I'd not use this, just send and int directly
let id = UUID()
var number = 0
}
It's a known problem. Nested observable objects are not supported yet in SwiftUI. I don't think you need ViewModel+Model here since ViewModel seems to be enough.
To make this work you have to trigger objectWillChange of your viewModel manually when objectWillChange of your model is triggered:
class ViewModel: ObservableObject {
init() {
number.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: &cancellables)
}
}
You better listen to only the object you care not the whole observable class if it is not needed.
Plus:
Since instead of injecting, you initialize your viewModel in your view, you better use StateObject instead of ObservedObject. See the reference from Apple docs: Managing model data in your app
One way you could handle this is to observe the publishers in your Storage class and send the objectWillChange publisher when it changes. I have done this in personal projects by adding a class that all my view models inherit from which provides a nice interface and handles the Combine stuff like this:
Parent ViewModel
import Combine
class ViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
func publish<T>(on publisher: Published<T>.Publisher) {
publisher.sink { [weak self] _ in self?.objectWillChange.send() }
.store(in: &cancellables)
}
}
Specific ViewModel
class ContentViewModel: ViewModel {
private let numberStorage = NumberStorage()
var number: Int { numberStorage.numberObject.number }
override init() {
super.init()
publish(on: numberStorage.$numberObject)
}
func increaseNumber() {
numberStorage.increaseNumber()
}
}
View
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(viewModel.number)")
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
Model/Storage
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func increaseNumber() {
self.numberObject.number += 1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
}
This results in the view re-rendering any time Storage.numberObject changes.
I'm trying to implement my app architecture loading in the app struct an object containing the data to be shared in all the views of the app through an environment object:
#main
struct SMT_testingApp: App {
#StateObject private var dataManager = DataManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataManager)
}
}
}
Here's the Datamanager class publishing the var containing the data:
class DataManager: ObservableObject {
#Published var SMTItemList: [SMTItem] = [SMTItem(id: UUID(), itemDesc: "", itemCreaDate: Date(), itemUpdDate: Date(), itemTags: [], linkedItemsUID: [])]
var urlFile: URL {
getDocumentsDirectory().appendingPathComponent("/SMT.json")
}
init() { loadData() }
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func loadData() {
//...
After here the View that contain the instance of his view model:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
List(viewModel.dataManager.SMTItemList) { item in
SMTItemView(item: item)
}
}
}
struct SMTItemView: View {
var item : SMTItem
var body: some View {
Text("Item desc: \(item.itemDesc)")
}
}
And finally, the view model that contains the environment object with the data.
extension ContentView {
class ViewModel: ObservableObject{
#EnvironmentObject var dataManager: DataManager
}
}
Now, the code is built correctly but at runtime I obtain this error in the content View:
What I'm doing wrong? Is correct implementing an architecture that way (one enviroment object with the data and many views/view models) ?
Thanks
#EnvironmentObject must be in view scope, not view model class. Like
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
#EnvironmentObject var dataManager: DataManager // << here !!
var body: some View {
List(dataManager.SMTItemList) { item in
SMTItemView(item: item)
}
}
}
I have 2 independent ObservableObjects called ViewModel1 and ViewModel2.
ViewModel2 has an array of strings:
#Published var strings: [String] = [].
Whenever that array is modified i want ViewModel1 to be informed.
What's the recommended approach to achieve this?
Clearly, there are a number of potential solutions to this, like the aforementioned NotificationCenter and singleton ideas.
To me, this seems like a scenario where Combine would be rather useful:
import SwiftUI
import Combine
class ViewModel1 : ObservableObject {
var cancellable : AnyCancellable?
func connect(_ publisher: AnyPublisher<[String],Never>) {
cancellable = publisher.sink(receiveValue: { (newStrings) in
print(newStrings)
})
}
}
class ViewModel2 : ObservableObject {
#Published var strings: [String] = []
}
struct ContentView : View {
#ObservedObject private var vm1 = ViewModel1()
#ObservedObject private var vm2 = ViewModel2()
var body: some View {
VStack {
Button("add item") {
vm2.strings.append("\(UUID().uuidString)")
}
ChildView(connect: vm1.connect)
}.onAppear {
vm1.connect(vm2.$strings.eraseToAnyPublisher())
}
}
}
struct ChildView : View {
var connect : (AnyPublisher<[String],Never>) -> Void
#ObservedObject private var vm2 = ViewModel2()
var body: some View {
Button("Connect child publisher") {
connect(vm2.$strings.eraseToAnyPublisher())
vm2.strings = ["Other strings","From child view"]
}
}
}
To test this, first try pressing the "add item" button -- you'll see in the console that ViewModel1 receives the new values.
Then, try the Connect child publisher button -- now, the initial connection is cancelled and a new one is made to the child's iteration of ViewModel2.
In order for this scenario to work, you always have to have a reference to ViewModel1 and ViewModel2, or at the least, the connect method, as I demonstrated in ChildView. You could easily pass this via dependency injection or even through an EnvironmentObject
ViewModel1 could also be changed to instead of having 1 connection, having many by making cancellable a Set<AnyCancellable> and adding a connection each time if you needed a one->many scenario.
Using AnyPublisher decouples the idea of having a specific types for either side of the equation, so it would be just as easy to connect ViewModel4 to ViewModel1, etc.
I had same problem and I found this method working well, just using the idea of reference type and taking advantage of class like using shared one!
import SwiftUI
struct ContentView: View {
#StateObject var viewModel2: ViewModel2 = ViewModel2.shared
#State var index: Int = Int()
var body: some View {
Button("update strings array of ViewModel2") {
viewModel2.strings.append("Hello" + index.description)
index += 1
}
}
}
class ViewModel1: ObservableObject {
static let shared: ViewModel1 = ViewModel1()
#Published var onReceiveViewModel2: Bool = Bool() {
didSet {
print("strings array of ViewModel2 got an update!")
print("new update is:", ViewModel2.shared.strings)
}
}
}
class ViewModel2: ObservableObject {
static let shared: ViewModel2 = ViewModel2()
#Published var strings: [String] = [String]() {
didSet { ViewModel1.shared.onReceiveViewModel2.toggle() }
}
}
I have an ObservableObject class used to fetch data from an api. It takes one parameter which is the api key. I am trying to pass that key from a parameter of my ContentView to the object.
class UnsplashAPI: ObservableObject {
//some code
var clientId: String
init(clientId: String) {
self.clientId = clientId
}
//some more code
}
This works fine when I'm asking for a parameter in my struct
struct ContentView: View {
#ObservedObject var api = UnsplashAPI(clientId: "APIKEY")
var body: some View {
//View
}
}
However this doesn't:
struct ContentView: View {
var clientId: String
#ObservedObject var api = UnsplashAPI(clientId: self.clientId) // Cannot find 'self' in scope
init(clientId: String){
self.clientId = clientId
}
var body: some View {
//View
}
}
I think i'm initialising the struct wrong as I am getting the error "Cannot find 'self' in scope"
Initialize it inside init
struct ContentView: View {
var clientId: String
#ObservedObject var api: UnsplashAPI
init(clientId: String){
self.clientId = clientId
self.api = UnsplashAPI(clientId: clientId) // << here !!
}
// ...