How to run a method in ContentView when an ObservedObject changes in other class [Swift 5 iOS 13.4] - ios

Here is my basic ContentView
struct ContentView: View
{
#ObservedObject var model = Model()
init(model: Model)
{
self.model = model
}
// How to observe model.networkInfo's value over here and run "runThis()" whenever the value changes?
func runThis()
{
// Function that I want to run
}
var body: some View
{
VStack
{
// Some widgets here
}
}
}
}
Here is my model
class Model: ObservableObject
{
#Published var networkInfo: String
{
didSet
{
// How to access ContentView and run "runThis" method from there?
}
}
}
I'm not sure if it is accessible ? Or if I can observe ObservableObject changes from View and run any methods?
Thanks in advance!

There are a number of ways to do this. If you want to runThis() when the
networkInfo changes then you could use something like this:
class Model: ObservableObject {
#Published var networkInfo: String = ""
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button(action: {
self.model.networkInfo = "test"
}) {
Text("change networkInfo")
}
}.onReceive(model.$networkInfo) { _ in self.runThis() }
}
func runThis() {
print("-------> runThis")
}
}
another global way is this:
class Model: ObservableObject {
#Published var networkInfo: String = "" {
didSet {
NotificationCenter.default.post(name: NSNotification.Name("runThis"), object: nil)
}
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button(action: {
self.model.networkInfo = "test"
}) {
Text("change networkInfo")
}
}.onReceive(
NotificationCenter.default.publisher(for: NSNotification.Name("runThis"))) { _ in
self.runThis()
}
}
func runThis() {
print("-------> runThis")
}
}

Related

Why is my .onAppear not getting triggered when an EnvironmentObject changes?

I'm trying to learn SwiftUI, but i can't seem to get my view to update. I want my WorkoutsView to refresh with the newly added workout when the user presses the "Add" button:
WorkoutTrackerApp:
#main
struct WorkoutTrackerApp: App {
var body: some Scene {
WindowGroup {
WorkoutTrackerView()
}
}
}
extension WorkoutTrackerApp {
struct WorkoutTrackerView: View {
#StateObject var workoutService = WorkoutService.instance
var body: some View {
NavigationView {
WorkoutsView { $workout in
NavigationLink(destination: WorkoutView(workout: $workout)){
Text(workout.title)
}
}
.toolbar {
Button("Add") {
workoutService.addNewWorkout()
}
}
.navigationTitle("Workouts")
}
.environmentObject(workoutService)
}
}
}
WorkoutsView:
import Foundation
import SwiftUI
struct WorkoutsView<Wrapper>: View where Wrapper: View {
#EnvironmentObject var workoutService: WorkoutService
#StateObject var viewModel: ViewModel
let workoutWrapper: (Binding<Workout>) -> Wrapper
init(_ viewModel: ViewModel = .init(), workoutWrapper: #escaping (Binding<Workout>) -> Wrapper) {
_viewModel = StateObject(wrappedValue: viewModel)
self.workoutWrapper = workoutWrapper
}
var body: some View {
List {
Section(header: Text("All Workouts")) {
ForEach($viewModel.workouts) { $workout in
workoutWrapper($workout)
}
}
}
.onAppear {
viewModel.workoutService = self.workoutService
viewModel.getWorkouts()
}
}
}
extension WorkoutsView {
class ViewModel: ObservableObject {
#Published var workouts = [Workout]()
var workoutService: WorkoutService?
func getWorkouts() {
workoutService?.getWorkouts { workouts in
self.workouts = workouts
}
}
}
}
WorkoutService:
import Foundation
class WorkoutService: ObservableObject {
static let instance = WorkoutService()
#Published var workouts = [Workout]()
private init() {
for i in 0...5 {
let workout = Workout(id: i, title: "Workout \(i)", exercises: [])
workouts.append(workout)
}
}
func getWorkouts(completion: #escaping ([Workout]) -> Void) {
DispatchQueue.main.async {
completion(self.workouts)
}
}
func addNewWorkout() {
let newWorkout = Workout(title: "New Workout")
workouts = workouts + [newWorkout]
}
}
The .onAppear in WorkoutsView only gets called once - when the view gets initialised for the first time. I want it to also get triggered when workoutService.addNewWorkout() gets called.
FYI: The WorkoutService is a 'mock' service, in the future i want to call an API there.
Figured it out, changed the body of WorkoutsView to this:
var body: some View {
List {
Section(header: Text("All Workouts")) {
ForEach($viewModel.workouts) { $workout in
workoutWrapper($workout)
}
}
}
.onAppear {
viewModel.workoutService = self.workoutService
viewModel.getWorkouts()
}
.onReceive(workoutService.objectWillChange) {
viewModel.getWorkouts()
}
}
Now the workouts list gets refreshed when workoutService publisher emits. The solution involved using the .onReceive to do something when the WorkoutService changes.

SwiftUI: List does not display the data

I tried to display the data of my server, the items appear and then disappeared a second after what?
Yet I'm displaying a static list that works ..
Look at the start of the video the bottom list:
My code:
struct HomeView: View {
#EnvironmentObservedResolve private var viewModel: HomeViewModel
var test = ["", ""]
var body: some View {
VStack {
Button(
action: {
},
label: {
Text("My Button")
}
)
List(test, id: \.self) { el in
Text("Work")
}
List(viewModel.items) { el in
Text("\(el.id)") // Not work
}
}
.padding()
.onAppear {
viewModel.getData()
}
}
}
My viewModel:
class HomeViewModel: ObservableObject {
private let myRepository: MyRepository
#Published var items: [Item] = []
init(myRepository: MyRepository) {
self.myRepository = myRepository
}
}
#MainActor extension HomeViewModel {
func getData() {
Task {
items = try await myRepository.getData()
}
}
}

SwiftUI MVVM Binding List Item

I am trying to create a list view and a detailed screen like this:
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
LazyVStack {
// https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
ForEach(viewModel.items.identifiableIndicies) { index in
MyListItemView($viewModel.items[index])
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = []
...
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item), label: {
...
})
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
#StateObject var viewModel: MyListViewItemDetailModel
init(item: Binding<Item>) {
viewModel = MyListViewItemDetailModel(item: item)
}
var body: some View {
...
}
}
class MyListViewItemDetailModel: ObservableObject {
var item: Binding<Item>
...
}
I am not sure what's wrong with it, but I found that item variables are not synced with each other, even between MyListItemDetailView and MyListItemDetailViewModel.
Is there anyone who can provide the best practice and let me know what's wrong in my implmentation?
I think you should think about a minor restructure of your code, and use only 1
#StateObject/ObservableObject. Here is a cut down version of your code using
only one StateObject source of truth:
Note: AFAIK Binding is meant to be used in View struct not "ordinary" classes.
PS: what is identifiableIndicies?
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Item: Identifiable {
let id = UUID().uuidString
var name: String = ""
}
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
var body: some View {
LazyVStack {
ForEach(viewModel.items.indices) { index in
MyListItemView(item: $viewModel.items[index])
}
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = [Item(name: "one"), Item(name: "two")]
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item)){
Text(item.name)
}
}
}
class MyAPIModel {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your fetching here
completion(Item(name: "new data from api"))
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
let myApiModel = MyAPIModel()
var body: some View {
VStack {
Button(action: fetchNewData) {
Text("Fetch new data")
}
TextField("edit item", text: $item.name).border(.red).padding()
}
}
func fetchNewData() {
myApiModel.fetchItemData() { itemData in
item = itemData
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyListView()
}.navigationViewStyle(.stack)
}
}
EDIT1:
to setup an API to call some functions, you could use something like this:
class MyAPI {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your stuff
}
}
and use it to obtain whatever data you require from the server.
EDIT2: added some code to demonstrate the use of an API.

SwiftUI three column navigation layout not closing when selecting same item

I'm using a three column navigation layout and facing the issue, that when selecting the same second column's item, the drawers won't close. If I take the files app as reference, selecting the same item again will close the drawer. Can someone tell me what's the issue? And is drawer the correct term?
Thanks in advance, Carsten
Code to reproduce:
import SwiftUI
extension UISplitViewController {
open override func viewDidLoad() {
preferredDisplayMode = .twoBesideSecondary
}
}
#main
struct TestApp: App {
#Environment(\.factory) var factory
var body: some Scene {
WindowGroup {
NavigationView {
ContentView(viewModel: factory.createVM1())
ContentView2(viewModel: factory.createVM2())
EmptyView()
}
}
}
}
struct FactoryKey: EnvironmentKey {
static let defaultValue: Factory = Factory()
}
extension EnvironmentValues {
var factory: Factory {
get {
return self[FactoryKey.self]
}
set {
self[FactoryKey.self] = newValue
}
}
}
class Factory {
func createVM1() -> ViewModel1 {
ViewModel1()
}
func createVM2() -> ViewModel2 {
ViewModel2()
}
func createVM3(from item: ViewModel2.Model) -> ViewModel3 {
ViewModel3(item: item)
}
}
class ViewModel1: ObservableObject {
struct Model: Identifiable {
let id: UUID = UUID()
let name: String
}
#Published var items: [Model]
init() {
items = (1 ... 4).map { Model(name: "First Column Item \($0)") }
}
}
struct ContentView: View {
#Environment(\.factory) var factory
#StateObject var viewModel: ViewModel1
var body: some View {
List {
ForEach(viewModel.items) { item in
NavigationLink(
destination: ContentView2(viewModel: factory.createVM2()),
label: {
Text(item.name)
})
}
}
}
}
class ViewModel2: ObservableObject {
struct Model: Identifiable {
let id: UUID = UUID()
let name: String
}
#Published var items: [Model]
init() {
items = (1 ... 4).map { Model(name: "Second Column Item \($0)") }
}
}
struct ContentView2: View {
#Environment(\.factory) var factory
#StateObject var viewModel: ViewModel2
var body: some View {
List {
ForEach(viewModel.items) { item in
NavigationLink(
destination: Detail(viewModel: factory.createVM3(from: item)),
label: {
Text(item.name)
})
}
}
}
}
class ViewModel3: ObservableObject {
let item: ViewModel2.Model
init(item: ViewModel2.Model) {
self.item = item
}
}
struct Detail: View {
#StateObject var viewModel: ViewModel3
var body: some View {
Text(viewModel.item.name)
}
}

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