ForEach in ScrollView not updating until interaction - swiftui

When adding to or removing from an array, the view does not update unless the user interacts with the ScrollView.
The Model is a ObservableObject that is declared as a StateObject early in the app lifecycle and then passed as a EnvironmentObject.
The data is simply a custom Profile object with an array of objects, when adding to store.profile!.tasks.append() or removing from the view does not update unless the user scrolls the ScrollView; I mean literally by 1 pixel.
What I have tried
Wrapping the ForEach in a LazyVStack or VStack
Wrapping the NavigationLink in a VStack
Making sure size is full height incase it needed to recalculate
Code
class Profile: Identifiable, Codable, Equatable
var tasks: [Task]
}
struct Task: Identifiable, Codable, Equatable {
var createdBy: String
var title: String
var date: Date
}
class Store : ObservableObject {
#Published var profile: Profile?
}
struct ListView: View {
#EnvironmentObject var store: Store
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .bottomTrailing) {
ScrollView(.vertical, showsIndicators: false){
ForEach(store.profile!.tasks.filter({ return $0.createdBy == store.profile!.uid}).indices, id: \.self) { index in
NavigationLink(destination: DetailView().environmentObject(store)) {
TasksRow().id(UUID())
}
}
}
.frame(maxWidth: geometry.size.width, alignment: .center)
.frame(maxHeight: geometry.size.height)
.background(Color.gray)
}
}
}

Your Profile is a reference type, so when you append task in it the reference to profile is not changed, so published does not work.
Use instead value type for profile (ie. struct)
struct Profile: Identifiable, Codable, Equatable
var tasks: [Task]
}
now appending task to profile will change profile itself, the publisher send event and view will refresh.

Related

Is this the right way for using #ObservedObject and #EnvironmentObject?

Cow you give me some confirmation about my understanding about #ObservedObject and #EnvironmentObject?
In my mind, using an #ObservedObject is useful when we send data "in line" between views that are sequenced, just like in "prepare for" in UIKit while using #EnvironmentObject is more like "singleton" in UIKit. My question is, is my code making the right use of these two teniques? Is this the way are applied in real development?
my model used as brain for funcions (IE urls sessions, other data manipulations)
class ModelClass_ViaObservedObject: ObservableObject {
#Published var isOn: Bool = true
}
class ModelClass_ViaEnvironment: ObservableObject {
#Published var message: String = "default"
}
my main view
struct ContentView: View {
//way to send data in views step by step
#StateObject var modelClass_ViaObservedObject = ModelClass_ViaObservedObject()
//way to share data more or less like a singleton
#StateObject var modelClass_ViaEnvironment = ModelClass_ViaEnvironment()
var myBackgroundColorView: Color {
if modelClass_ViaObservedObject.isOn {
return Color.green
} else {
return Color.red
}
}
var body: some View {
NavigationView {
ZStack {
myBackgroundColorView
VStack {
NavigationLink(destination:
SecondView(modelClass_viaObservedObject: modelClass_ViaObservedObject)
) {
Text("Go to secondary view")
.padding()
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.black, lineWidth: 1)
)
}
Text("text received from second view: \(modelClass_ViaEnvironment.message)")
}
}
.navigationTitle("Titolo")
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(modelClass_ViaEnvironment)
}
}
my second view
struct SecondView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var modelClass_viaObservedObject: ModelClass_ViaObservedObject
//global data in environment, not sent step by step view by view
#EnvironmentObject var modelClass_ViaEnvironment: ModelClass_ViaEnvironment
var body: some View {
VStack(spacing: 5) {
Text("Second View")
Button("change bool for everyone") {
modelClass_viaObservedObject.isOn.toggle()
dismiss()
}
TextField("send back", text: $modelClass_ViaEnvironment.message)
Text(modelClass_ViaEnvironment.message)
}
}
}
No, we use #State for view data like if a toggle isOn, which can either be a single value itself or a custom struct containing multiple values and mutating funcs. We pass it down the View hierarchy by declaring a let in the child View or use #Binding var if we need write access. Regardless of if we declare it let or #Binding whenever a different value is passed in to the child View's init, SwiftUI will call body automatically (as long as it is actually accessed in body that is).
#StateObject is for when a single value or a custom struct won't do and we need a reference type instead for view data, i.e. if persisting or syncing data (not using the new async/await though because we use .task for that). The object is init before body is called (usually before it is about to appear) and deinit when the View is no longer needed (usually after it disappears).
#EnvironmentObject is usually for the store object that holds model structs in #Published properties and is responsible for saving or syncing,. The difference is the model data is not tied to any particular View, like #State and #StateObject are for view data. This object is usually a singleton, one for the app and one with sample data for when previewing, because it should never be deinit. The advantage of #EnvironmentObject over #ObservedObject is we don't need to pass it down through each View as a let that don't need the object when we only need it further down the hierarchy. Note the reason it has to be passed down as a let and not #ObservedObject is then body would be needlessly called in the intermediate Views because SwiftUI's dependency tracking doesn't work for objects only value types.
Here is some sample code:
struct MyConfig {
var isOn = false
var message = ""
mutating func reset() {
isOn = false
message = ""
}
}
struct MyView: View {
#State var config = MyConfig() // grouping vars into their struct makes use of value semantics to track changes (a change to any of its properties is detected as a change to the struct itself) and offers testability.
var body: some View {
HStack {
ViewThatOnlyReads(config: config)
ViewThatWrites(config: $config)
}
}
}
struct ViewThatOnlyReads: View {
let config: MyConfig
var body: some View {
Text(config.isOn ? "It's on" : "It's off")
}
}
struct ViewThatWrites: View {
#Binding var config: MyConfig
var body: some View {
Toggle("Is On", isOn: $config.isOn)
}
}

Update LazyVGrid list

I have a LazyVGrid list which need to be updated whenever a data is updated. The data is in a singleton class.
class BluetoothManager: NSObject {
static let shared = BluetoothManager()
#objc dynamic private(set) var stations: [BHStation] = []
}
And the list is
var body: some View {
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: UIDevice.current.userInterfaceIdiom == .pad ? 2 : 1)
ScrollView {
LazyVGrid(columns: columns, alignment: .center, spacing: 10, pinnedViews: [], content: {
ForEach(BluetoothManager.shared.stations, id: \.peripheral.identifier) { item in
NavigationLink(destination: DetailView()) {
MainCell()
}
}
})
}
}
I tried to use ObservableObject with #Published, or to use #State/#Binding but none of them worked.
How can I make the list get updated whenever stations is get updated? #objc dynamic is necessary to be used in the other UIKit classes.
In order for your SwiftUI View to know to update, you should use an ObservableObject with a #Published property and store it as a #ObservedObject or #StateObject:
class BluetoothManager: NSObject, ObservableObject {
static let shared = BluetoothManager()
private override init() { }
#Published var stations: [BHStation] = []
}
struct ContentView : View {
#ObservedObject private var manager = BluetoothManager.shared
var body: some View {
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: UIDevice.current.userInterfaceIdiom == .pad ? 2 : 1)
ScrollView {
LazyVGrid(columns: columns, alignment: .center, spacing: 10, pinnedViews: [], content: {
ForEach(manager.stations, id: \.peripheral.identifier) { item in //<-- Here
NavigationLink(destination: DetailView()) {
MainCell()
}
}
})
}
}
}
The #Published may interfere with your #objc requirement, but since you haven't given any information on how you use it in UIKit or why it's necessary, it's not possible to say directly what the fix is. You may need a different setter to use when interacting with it in UIKit.
Also, be aware that #Published won't update as expected unless the model type it is storing (BHStation) is a struct. If it's a class, you may need to call objectWillChange directly to get the #Published publisher to trigger correctly.

SwiftUI AppStorage, UserDefaults and ObservableObject

I have two different views, ContentView and CreateView.
In CreateView, I get user's inputs by textfield, and once user clicks on Save button, the inputs will be stored in AppStorage.
Then, I want to display the saved inputs on ContentView.
Here, I tried to use State & Binding but it didn't work out well.
How would I use the variable, that is created in CreateView, in ContentView?
what property should I use..
Thanks
Here's the updated questions with the code...
struct ContentView: View {
// MARK: - PROPERTY
#ObservedObject var appData: AppData
let createpage = CreatePage(appData: AppData())
var body: some View {
HStack {
NavigationLink("+ create a shortcut", destination: CreatePage(appData: AppData()))
.padding()
Spacer()
} //: HStack - link to creat page
VStack {
Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
}
}
struct CreatePage: View {
// MARK: - PROPERTY
#AppStorage("title") var currentShortcutTitle: String?
#AppStorage("option") var currentOption: String?
#Environment(\.presentationMode) var presentationMode
#ObservedObject var appData: AppData
var body: some View {
NavigationView{
ScrollView{
Text("Create a ShortCut")
.padding()
HStack {
TextField("what is the title?", text: $appData.titleInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
//.frame(width: 150, height: 60, alignment: .center)
.border(Color.black)
.padding()
} //: HStack - Textfield - title
.padding()
HStack (spacing: 10) {
TextField("options?", text: $appData.optionInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80, height: 40, alignment: .leading)
.padding()
} //: HStack - Textfield - option
.padding()
Button(action: {
self.appData.shortcutTitle = self.appData.titleInput
self.appData.shortcutOption = self.appData.optionInput
UserDefaults.standard.set(appData.shortcutTitle, forKey: "title")
UserDefaults.standard.set(appData.shortcutOption, forKey: "option")
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
.padding()
.frame(width: 120, height: 80)
.border(Color.black)
}) //: Button - save
.padding(.top, 150)
} //: Scroll View
} //: Navigation View
} //: Body
class AppData: ObservableObject {
#Published var shortcutTitle : String = "Deafult Shortcut"
#Published var shortcutOption : String = "Default Option"
#Published var titleInput : String = ""
#Published var optionInput : String = ""
}
So the problem here is that
when I put new inputs on CreatePage and tab the save button, the new inputs do not appear on ContentView page.The output keeps showing the default values of title and option, not user inputs.
If user makes a new input and hit the save button, I want to store them in AppStorage, and want the data to be kept on ContentView (didn't make the UI yet). Am I using the AppStorage and UserDefaults in a right direction?
If anyone have insights on these issues.. would love to take your advice or references.
You're creating instances of AppData in multiple places. In order to share data, you have to share one instance of AppData.
I'm presuming that you create AppData in a parent view of ContentView since you have #ObservedObject var appData: AppData defined at the top of the view (without = AppData()). This is probably in your WindowGroup where you also must have a NavigationView.
I removed the next (let createpage = CreatePage(appData: AppData())) because it does nothing. And in the NavigationLink, I passed the same instance of AppData.
struct ContentView: View {
// MARK: - PROPERTY
#StateObject var appData: AppData = AppData() //Don't need to have `= AppData()` if you already create it in a parent view
var body: some View {
// I'm assuming there's a NavigationView in a parent view
VStack { //note that I've wrapped the whole view in a VStack to avoid having two root nodes (which can perform differently in NavigationView depending on the platform)
HStack {
NavigationLink("+ create a shortcut", destination: CreatePage(appData: appData))
.padding()
Spacer()
} //: HStack - link to creat page
VStack {
Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
}
}
}
}
struct CreatePage: View {
// MARK: - PROPERTY
#AppStorage("title") var currentShortcutTitle: String?
#AppStorage("option") var currentOption: String?
#Environment(\.presentationMode) var presentationMode
#ObservedObject var appData: AppData
var body: some View {
NavigationView{
ScrollView{
Text("Create a ShortCut")
.padding()
HStack {
TextField("what is the title?", text: $appData.titleInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
//.frame(width: 150, height: 60, alignment: .center)
.border(Color.black)
.padding()
} //: HStack - Textfield - title
.padding()
HStack (spacing: 10) {
TextField("options?", text: $appData.optionInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80, height: 40, alignment: .leading)
.padding()
} //: HStack - Textfield - option
.padding()
Button(action: {
appData.shortcutTitle = appData.titleInput
appData.shortcutOption = appData.optionInput
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
.padding()
.frame(width: 120, height: 80)
.border(Color.black)
}) //: Button - save
.padding(.top, 150)
} //: Scroll View
} //: Navigation View
} //: Body
}
Regarding #AppStorage and UserDefaults, it's a little hard to tell what your intent is at this point with those. But, you shouldn't need to declare AppStorage and call UserDefaults on the same key -- #AppStorage writes to UserDefaults for you. Read more at https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper
you can use a singleton ObservableObject that conforms to NSObject so you can observe everything even older apple objects like progress.
class appData : NSObject , ObservableObject {
static let shared = appData()
#Published var localItems = Array<AVPlayerItem>()
#Published var fractionCompleted : Double = 0
#Published var downloaded : Bool = false
#Published var langdentifier = UserDefaults.standard.value(forKey: "lang") as? String ?? "en" {
didSet {
print("AppState isLoggedIn: \(langIdentifier)")
}
}
var progress : Progress?
override init() {
}
}
then u can use it anywhere in your code like this:
appData.shared.langIdentifier == "en" ? .leading : .trailing
You should be able to simply put AppStorage objects in the ObservableClass and call them from there. There should be no need to put AppStorage in the View and then read from UserDefaults in the class.
class AppData: ObservableObject {
#AppStorage(keyForValue) var valueForKey: ValueType = defaultValue
...
}
Of course, you could make #Published property in the class and define the getter and setter for it so it reads and writes directly to the UserDefaults but at that point, you're just creating more work than simply using AppStorage from the beginning directly in the class.

Pushing multiple navigation links from a parent view in SwiftUI

I want to implement a wizard whereby the user has to go through multiple screens in order to complete a signup process.
In SwiftUI the easiest way to do this is to have each view when it's finished push the next view on the navigation stack, but this codes the entire navigation between views in the views themselves, and I would like to avoid it.
What I want to do is have a parent view show the navigation view and then push the different steps on that navigation view.
I have something working already that looks like this:
struct AddVehicleView: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
NavigationView {
switch viewModel.state {
case .description:
AddDescriptionView(addDescriptionViewModel: AddVehicleDescriptionViewModel(), addVehicleViewModel: viewModel)
case .users:
AddUsersView(viewModel: AddUsersViewModel(viewModel.vehicle), addVehicleViewModel: viewModel)
}
}
}
}
This works fine. In the first step the AddVehicleViewModel is updated with the necessary info, the AddVehicleView is re-evaluated, the switch case jumps to the next option and the next view is presented to complete the wizard.
The issue with this however is that there are no navigation stack animations. Views simply get replaced. How can I change this to a system whereby the views are pushed, without implementing the push inside the AddDescriptionView object?
Should I write wrapper views that do the navigation stack handling on top of those views, and get rid of the switch case?
Ok so if you want to go from view a to b you should implement this not in your NavigationView but the view after the NavigationView, this way you wont break the animations. Why? Good question, I really don't know. When possible I keep my NavigationView always in the App struct under WindowGroup.
To get back to the point. Basically there should be an intermediate view between your steps and NavigationView. This view (StepperView) will contain the navigation logic of your steps. This way you keep the animations intact.
import SwiftUI
class AddVehicleViewModel: ObservableObject {
enum StateType {
case description
case users1
case users2
}
#Published var state: StateType? = nil
}
struct AddDescriptionView: View {
#ObservedObject var viewModel: AddVehicleViewModel
#State var text: String = ""
var body: some View {
GeometryReader {proxy in
VStack {
TextField("test", text: self.$text).background(RoundedRectangle(cornerRadius: 10).fill(Color.white).frame(width: 150, height: 40)).padding()
Button("1") {
viewModel.state = .users1
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center).background(Color.orange)
}
}
}
struct AddUsersView: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
GeometryReader {proxy in
ZStack {
Button("2") {
viewModel.state = .users2
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/).background(Color.orange)
}
}
}
struct AddUsersView2: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
GeometryReader {proxy in
ZStack {
Button("3") {
viewModel.state = .description
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/).background(Color.orange)
}
}
}
struct StepperView: View {
#ObservedObject var viewModel: AddVehicleViewModel = AddVehicleViewModel()
var body: some View {
VStack {
NavigationLink(
destination: AddDescriptionView(viewModel: viewModel),
isActive: .constant(viewModel.state == .description),
label: {EmptyView()})
if viewModel.state == .users1 {
NavigationLink(
destination: AddUsersView(viewModel: viewModel),
isActive: .constant(true),
label: {EmptyView()})
}
if viewModel.state == .users2 {
NavigationLink(
destination: AddUsersView2(viewModel: viewModel),
isActive: .constant(true),
label: {EmptyView()})
}
}.onAppear {
viewModel.state = .description
}
}
}
class BackBarButtonItem: UIBarButtonItem {
#available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// Don't set the menu here
// super.menu = menu
}
get {
return super.menu
}
}
}
struct AddVehicleView: View {
#ObservedObject var viewModel: AddVehicleViewModel = AddVehicleViewModel()
var body: some View {
NavigationView {
NavigationLink(
destination: StepperView(),
isActive: .constant(true),
label: {EmptyView()})
}
}
}

.onAppear is calling when I navigated back to a view by clicking back button

I have two views written in swiftUI , say for example ViewA and ViewB.
onAppear() of ViewA has an apiCall which calls when initially the view is loaded.
I navigate to ViewB from ViewA using navigation link and on clicking back button in ViewB the onAppear() of ViewA is called.
• Is there any way to stop calling onAppear() while navigated back from a view
• I am looking swiftUI for something like 'ViewDidLoad' in UIKit
given a sample of my code
struct ContentView: View {
var body: some View {
NavigationView{
List(viewModel.list){ item in
NavigationLink(
destination: Text("Destination"),
label: {
Text(item.name)
})
}
.onAppear{
viewModel.getListApiCall()
}
}
}
}
Overview
SwiftUI is quite different from the way UIKit works.
It would be best to watch the tutorials (links below) to understand how SwiftUI and Combine works.
SwiftUI is a declarative framework so the way we approach is quite different. It would be best not to look for a direct comparison to UIKit for equivalent functions.
Model:
Let the model do all the work of fetching and maintaining the data
Ensure that your model conforms to ObservableObject
When ever any #Published property changes, it would imply that the model has changed
View:
Just display the contents of the model
By using #ObservedObject / #EnvironmentObject SwiftUI would observe the model and ensure that the view states in sync with any changes made to the model
Notice that though the model fetches the data after 2 seconds, the view reacts to it and displays the updated data.
Model Code:
class Model: ObservableObject {
#Published var list = [Item]()
init() {
fetchItems()
}
private func fetchItems() {
//To simulate some Async API call
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.list = (1...10).map { Item(name: "name \($0)") }
}
}
}
struct Item: Identifiable {
var name: String
var id : String {
name
}
}
View Code:
import SwiftUI
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView{
List(model.list){ item in
NavigationLink(destination: Text("Destination")) {
Text(item.name)
}
}
}
}
}
Reference:
SwiftUI
https://developer.apple.com/videos/play/wwdc2020/10119
https://developer.apple.com/videos/play/wwdc2020/10037
https://developer.apple.com/videos/play/wwdc2020/10040
Combine
https://developer.apple.com/videos/play/wwdc2019/722
https://developer.apple.com/videos/play/wwdc2019/721
https://developer.apple.com/videos/play/wwdc2019/226
You could add a variable to check if the getListApiCall() has been invoked.
struct ContentView: View {
#State var initHasRun = false
var body: some View {
NavigationView{
List(viewModel.list){ item in
NavigationLink(
destination: Text("Destination"),
label: {
Text(item.name)
})
}
.onAppear{
if !initHasRun {
viewModel.getListApiCall()
initHasRun=true
}
}
}
}
}