SwiftUI - Subclassed viewModel doesn't trigger view refresh - swiftui

I have this situation where I have a a BaseView containing some common elements and a BaseViewModel containing some common functions, but when its #Published var get updated no BaseView refresh occurs.
The setup is this:
class BaseViewModel: ObservableObject {
#Published var overlayView: AnyView = EmptyView().convertToAnyView()
func forceViewRefresh() {
self.objectWillChange.send()
}
func setOverlayView(overlayView: AnyView) {
self.overlayView = overlayView
}
}
This view model subclasses BaseViewModel:
class FirstViewModel: BaseViewModel {
func showOverlayView() {
self.setOverlayView(overlayView: OverlayView().convertToAnyView())
}
}
also I have a BaseView where I use the overlayView
struct BaseView<Content: View>: View {
let content: Content
#ObservedObject var viewModel = BaseViewModel()
init(content: () -> Content) {
self.content = content()
}
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.vertical)
content
viewModel.overlayView
}
}
}
The first view that gets displayed is FirstView, which conforms to a BaseViewProtocol and has a FirstViewModel that extends BaseViewModel.
struct FirstView: BaseViewProtocol {
#ObservedObject var viewModel = FirstViewModel()
var body: some View {
BaseView() {
Button("Show overlay") {
viewModel.showOverlayView()
}
}
}
}
Clicking the Show overlay button in First View calls the showOverlayView() func on FirstViewModel which in turn calls setOverlayView on the BaseViewModel. The value of overlayView in BaseViewModel changes as expected, but no view refresh on FirstView is called.
What am I doing wrong?
Thanks a lot.

I have just tested this code sample and works fine on Xcode 12 beta 6 & iOS 14 beta 8
struct ContentView: View {
#StateObject private var viewModel = FirstViewModel()
var body: some View {
ZStack {
Button(action: { viewModel.showOverlayView() }) {
Text("Press")
}
viewModel.overlayView
}
}
}
class BaseViewModel: ObservableObject {
#Published var overlayView: AnyView = AnyView(EmptyView())
func forceViewRefresh() {
self.objectWillChange.send()
}
func setOverlayView(overlayView: AnyView) {
self.overlayView = overlayView
}
}
class FirstViewModel: BaseViewModel {
func showOverlayView() {
self.setOverlayView(
overlayView: AnyView(
Color.blue
.opacity(0.2)
.allowsHitTesting(false)
)
)
}
}

Generally in SwiftUI you don't create views in outside the body. The view creation should be left to SwiftUI - instead you can define some other controls telling SwiftUI how and when to create a view.
Here is a simplified demo how to present different overlays for different views.
You can create a basic OverlayView:
enum OverlayType {
case overlay1, overlay2
}
struct OverlayView: View {
let overlayType: OverlayType
#ViewBuilder
var body: some View {
if overlayType == .overlay1 {
Text("Overlay1") // can be replaced with any view you want
}
if overlayType == .overlay2 {
Text("Overlay1")
}
}
}
and use it in your BaseView (if overlayType is nil the overlay will not be shown):
struct BaseView<Content>: View where Content: View {
let overlayType: OverlayType?
let content: () -> Content
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.vertical)
content()
if overlayType != nil {
OverlayView(overlayType: overlayType!)
}
}
}
}
Now in the ContentView you can use the BaseView and specify its OverlayType.
struct ContentView: View {
#State var overlayType: OverlayType?
var body: some View {
BaseView(overlayType: overlayType) {
Button("Show overlay") {
overlayType = .overlay1
}
}
}
}
Some considerations:
For simplicity I used #State variables to control overlays. If there are other use cases for your ViewModels you may want to move the logic there.
Note that instead of AnyView it's preferred to use #ViewBuilder.
Also if you want to observe an ObservableObject inside a view, you need to use #ObservedObject, not #ObservableObject.

Related

How to establish communication between ViewModels of the same screen in SwiftUI MVVM

In my app, I have a Screen with Toolbar and Main View.
VStack {
ToolbarView()
MainView()
}
Think of it like this:
Toolbar has its own View and ToolbarViewModel where we can select “Tools”
struct ToolbarView: View {
#StateObject private var VM = ToolbarViewModel()
var body: some View {
Text("Toolbar View")
}
}
#MainActor final class ToolbarViewModel: ObservableObject {
var selectedTool: Int = 1
func selectTool() {
//We select a new tool
selectedTool = 2
}
}
Main view has its own View and MainViewModel
struct MainView: View {
#StateObject private var VM = MainViewModel()
var body: some View {
Text("Main View")
}
}
#MainActor final class MainViewModel: ObservableObject {
var selectedTool: Int = 1
}
Now, when I tap a button in the ToolbarView and call a function in ToolbarViewModel to select a new tool, the tool must change in the MainViewModel too.
What would be the correct way of implementing this?
In the screen with the MainView and ToolbarView instances, create #StateObject(s) for both
#StateObject private var mainVM = MainViewModel()
#StateObject private var toolbarVM = ToolbarViewModel()
Then, create Observed objects in both your instances:
ToolbarView:
struct ToolbarView: View {
#ObservedObject var VM: ToolbarViewModel
var body: some View {
Text("Toolbar View")
}
}
MainView:
struct MainView: View {
#ObservedObject var VM: MainViewModel
var body: some View {
Text("Main View")
}
}
Then pass your objects in the screen than you created your instances:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
Finally, whenever you make a change you can just listen to it like:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
.onChange(of: toolbarVM.isDrawing) { newValue in {
mainVM.isDrawing = newValue
}
.onChange(of: mainVM.isDrawing) { newValue in {
toolbarVM.isDrawing = newValue
}
We don't use view model objects in SwiftUI. The View data struct is already the view model that SwiftUI uses to create/update/remove UIView objects automatically for us. The property wrappers give the struct reference semantics giving us the best of both worlds. You'll have to learn #State and #Binding and put the shared state in a parent View, then pass it down as a let for read access or #Bindng var for write access, e.g.
#State var tools = Tools()
...
VStack {
ToolbarView(tools: $tools)
MainView(tools: tools)
}
struct Tools {
var selectedTool: Int = 1
mutating func selectTool() {
//We select a new tool
selectedTool = 2
}
}
struct MainView: View {
let tools: Tools
var body: some View {
Text("Main View \(tools.selected)")
}
}
struct ToolbarView: View {
#Binding var tools: Tools
var body: some View {
Text("Toolbar View")
Button("Select") {
tools.selectTool()
}
}
}

SwiftUI: Must an ObservableObject be passed into a View as an EnvironmentObject?

If I create an ObservableObject with a #Published property and inject it into a SwifUI view with .environmentObject(), the view responds to changes in the ObservableObject as expected.
class CounterStore: ObservableObject {
#Published private(set) var counter = 0
func increment() {
counter += 1
}
}
struct ContentView: View {
#EnvironmentObject var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Tapping on "Increment" will increase the count.
However, if I don't use the EnvironmentObject and instead pass the store instance into the view, the compiler does not complain, the store method increment() is called when the button is tapped, but the count in the View does not update.
struct ContentViewWithStoreAsParameter: View {
var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter) (DOES NOT UPDATE)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Here's how I'm calling both Views:
#main
struct testApp: App {
var store = CounterStore()
var body: some Scene {
WindowGroup {
VStack {
ContentView().environmentObject(store) // works
ContentViewWithStoreAsParameter(store: store) // broken
}
}
}
}
Is there a way to pass an ObservableObject into a View as a parameter? (Or what magic is .environmentalObject() doing behind the scenes?)
It should be observed somehow, so next works
struct ContentViewWithStoreAsParameter: View {
#ObservedObject var store: CounterStore
//...
You can pass down your store easily as #StateObject:
#main
struct testApp: App {
#StateObject var store = CounterStore()
var body: some Scene {
WindowGroup {
VStack {
ContentView().environmentObject(store) // works
ContentViewWithStoreAsParameter(store: store) // also works
}
}
}
}
struct ContentViewWithStoreAsParameter: View {
#StateObject var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter)") // now it does update
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
However, the store should normally only be available for the views that need it, why this solution would make the most sense in this context:
struct ContentView: View {
#StateObject var store = CounterStore()
var body: some View {
VStack {
Text("Count: \(store.counter)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}

Missing argument for parameter 'View Call' in call

I am struggle with understanding about why i have to give Popup view dependency named vm while calling this view since it is observable
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView() /// this line shows error
}
}
}
struct DetailView:View {
#ObservedObject var vm:ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
You have to set your vm property when you init your View. Which is the usual way.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView(vm: ViewModel()) // Initiate your ViewModel() and pass it as DetailView() parameter
}
}
}
struct DetailView:View {
var vm: ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
Or you could use #EnvironmentObject. You have to pass an .environmentObject(yourObject) to the view where you want to use yourObject, but again you'll have to initialize it before passing it.
I'm not sure it's the good way to do it btw, as an environmentObject can be accessible to all childs view of the view you declared the .environmentObject on, and you usually need one ViewModel for only one View.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView().environmentObject(ViewModel()) // Pass your ViewModel() as an environmentObject
}
}
}
struct DetailView:View {
#EnvironmentObject var vm: ViewModel // you can now use your vm, and access it the same say in all childs view of DetailView
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}

How to access to SwiftUI content view in extension delegate on Apple Watch?

I need to call loadData in my ContentView when the app becomes active. ExtensionDelegate is a class which handle app events such as applicationDidBecomeActive. But I don't understand how to get ContentView inside ExtensionDelegate.
This is my ContentView:
struct ContentView: View {
let network = Network()
#State private var currentIndex: Int = 0
#State private var sources: [Source] = []
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
}
func loadData() {
network.getSources { response in
switch response {
case .result(let result):
self.sources = result.results
case .error(let error):
print(error)
}
}
}
}
And ExtensionDelegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
}
func applicationDidBecomeActive() {
// Here I need to call 'loadData' of my ContentView
}
func applicationWillResignActive() {
}
...
The simplest solution as I see would be to use notification
in ContentView
let needsReloadNotification = NotificationCenter.default.publisher(for: .needsNetworkReload)
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
.onReceive(needsReloadNotification) { _ in self.loadData()}
}
and in ExtensionDelegate
func applicationDidBecomeActive() {
NotificationCenter.default.post(name: .needsNetworkReload, object: nil)
}
and somewhere in shared
extension Notification.Name {
static let needsNetworkReload = Notification.Name("NeedsReload")
}

Ambiguous reference to member 'subscript' in VStack Swift UI 5

I try to run a function in a VStack statement but it don't work. When I run it in a button (with the action label) it work perfectly. How can I insert my func in a VStack?
I declare a QuizData class:
class QuizData: ObservableObject {
var allQuizQuestion: [QuizView] = [QuizView]()
let objectWillChange = PassthroughSubject<QuizData,Never>()
var currentQuestion: Int = 0 {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
and I use it there :
struct Quiz: View {
var continent: Continent
#EnvironmentObject var quizData: QuizData
var body: some View {
VStack
{
generateQuiz(continent: continent, quizData: self.quizData)
quizData.allQuizQuestion[quizData.currentQuestion]
}
.navigationBarTitle (Text(continent.name), displayMode: .inline)
}
}
The func generateQuiz is:
func generateQuiz(continent: Continent, quizData: QuizData) -> Void {
var capital: [Capital]
var alreadyUse: [Int]
for country in CountryData {
if country.continentId == continent.id
{
alreadyUse = [Int]()
capital = [Capital]()
capital.append(CapitalData[country.id])
for _ in 1...3 {
var index = Int.random(in: 1 ... CapitalData.count - 1)
while alreadyUse.contains(index) {
index = Int.random(in: 1 ... CapitalData.count - 1)
}
capital.append(CapitalData[index])
}
capital.shuffle()
quizData.allQuizQuestion.append(QuizView(country: country, question: QuestionData[country.id], capital: capital))
}
}
quizData.allQuizQuestion.shuffle()
}
I need to generate quiz question before the view appear. How should I do this?
First, you can't call a function that doesn't return some View in a VStack closure because that closure is not a normal closure, but a #ViewBuilder closure:
#functionBuilder
struct ViewBuilder {
// Build a value from an empty closure, resulting in an
// empty view in this case:
func buildBlock() -> EmptyView {
return EmptyView()
}
// Build a single view from a closure that contains a single
// view expression:
func buildBlock<V: View>(_ view: V) -> some View {
return view
}
// Build a combining TupleView from a closure that contains
// two view expressions:
func buildBlock<A: View, B: View>(_ viewA: A, viewB: B) -> some View {
return TupleView((viewA, viewB))
}
// And so on, and so forth.
...
}
It's a Swift 5.1 feature that lets you do things like these:
VStack {
Image(uiImage: image)
Text(title)
Text(subtitle)
}
With which you can easily create a view from several other views. For further information take a look at https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api
Now, if I get your issue (correct me if I'm wrong) you need to call a function before your view appears to generate some data. Honestly I'd prefer to pass that data to the view from the outside (creating the data before the view creation). But if you really need it you can do something like:
struct ContentView: View {
private var values: [Int]! = nil
init() {
values = foo()
}
var body: some View {
List(values, id: \.self) { val in
Text("\(val)")
}
}
func foo() -> [Int] {
[0, 1, 2]
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Using the struct init and calling the function at the view creation.
EDIT: To answer your comment here below and since you are using an #EnvironmentObject you can do:
class ContentViewModel: ObservableObject {
#Published var values: [Int]!
init() {
values = generateValues()
}
private func generateValues() -> [Int] {
[0, 1, 2]
}
}
struct ContentView: View {
#EnvironmentObject var contentViewModel: ContentViewModel
var body: some View {
List(contentViewModel.values, id: \.self) { val in
Text("\(val)")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ContentViewModel()) //don't forget this
}
}
#endif
And in your SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: ContentView()
.environmentObject(ContentViewModel()) //don't forget this
)
self.window = window
window.makeKeyAndVisible()
}
}
This way you are creating a view model for your view and that view model will be accessible throughout your view hierarchy. Every time your view model will change your view will change too.