When a user wants to create a new record, I'd like to a) go to a new view, b) create the record during .onAppear, and then c) have the view update using the newly created record.
The first part is easy. I have a NavigationLink that goes to the new view along with the user name as a property. And I can create a new record during .onAppear. But it's the last part where things get tricky.
I have tried to create the record and switch views with a simultaneous gesture, I've tried loading the new view with a toggle view function, and about a dozen less inspired ideas. The problem always comes down to this: How do I get the FetchRequest to update after the new record has been created? Note: I need a reference to the newly created record so that the user can add things to the record.
Here's the code so far...
struct RecordView: View {
#Environment(\.managedObjectContext) var moc
let appDelegte = UIApplication.shared.delegate as! AppDelegate
var fetchedRecords: FetchRequest<Record>
var currentUser: User
init(recordNumber: String) {
fetchedRecords = FetchRequest<Record>(entity: Record.entity(), sortDescriptors: [], predicate: NSPredicate(format: "user.recordNumber = %#", recordNumberNumber))
}
var body: some View {
Text(fetchedRecords.wrappedValue.first.recordNumber)
}
.onAppear {
self.CreateNewRecord()
}
func CreateNewRecord() {
let newRecord = Record(context: self.moc)
newRecord.id = UUID()
let recordNumber = "AABBCCDDEE"
user.addToRecords(newRecord)
appDelegate.saveContext()
//This is when the fetched request should fetch the new record and the Text view should be updated.
}
}
I am a bit confused about the bit where you say you want to create a record onAppear whilst a user created a record? Anyways I would probably create a variable for the text and have it set to the new record inside the onAppear function. Although the whole onAppear create record still confuses me and might make the implementation of the variable more difficult.
What finally worked was combining a NavigationLink with a Button as explained by #kontiki. That allowed me to create a new record before going to the new view.I'm not sure this is the best solution, but it definitely works! The code below shows the theory behind the solution.
#State private var presentMe = false
#State var currentNumber = 10
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SecondView(number: currentNumber), isActive: $presentMe) { EmptyView() }
Button(action: {
//This is where I created a new record and saved to Core Data
self.currentNumber = 5
//I was then able to pass the new record to the second view through the navigation link
self.presentMe = true
}) {
Text("Go to SecondView")
}
}
}
}
}
struct SecondView: View {
var number: Int
var body: some View {
Text("The number is now = \(number)") //The number is now = 5
}
}
Related
I'm using the refreshable modifier on List
https://developer.apple.com/documentation/SwiftUI/View/refreshable(action:)
The List is contained in a view (TestChildView) that has a parameter. When the parameter changes, TestChildView is reinstantiated with the new value. The list has a refreshable action. However, when pulling down to refresh the list, the refresh action is run against the original view instance, so it doesn't see the current value of the parameter.
To reproduce with the following code: If you click the increment button a few times, you can see the updated value propagating to the list item labels. However, if you pull down the list to refresh, it prints the original value of the parameter.
I assume this is happening because of how refreshable works .. it sets the refresh environment value, and I guess it doesn't get updated as new instances of the view are created.
It seems like a bug, but I'm looking for a way to work around -- how can the refreshable action see the current variable/state values?
import SwiftUI
struct TestParentView: View {
#State var myVar = 0
var body: some View {
VStack {
Text("increment")
.onTapGesture {
myVar += 1
}
TestChildView(myVar: myVar)
}
}
}
struct TestChildView: View {
let myVar: Int
struct Item: Identifiable {
var id: String {
return val
}
let val: String
}
var list: [Item] {
return [Item(val: "a \(myVar)"), Item(val: "b \(myVar)"), Item(val: "c \(myVar)")]
}
var body: some View {
VStack {
List(list) { elem in
Text(elem.val)
}.refreshable {
print("myVar: \(myVar)")
}
}
}
}
The value of myVar in TestChildView is not updated because it has to be a #Binding. Otherwise, a new view is recreated.
If you pass the value #State var myVar from TestParentView to a #Binding var myVar to TestChildView, you will have the value being updated and the view kept alive the time of the parent view.
You will then notice that the printed value in your console is the refreshed one of the TestChildView.
Here is the updated code (See comments on the updated part).
import SwiftUI
struct TestParentView: View {
#State var myVar = 0
var body: some View {
VStack {
Text("increment")
.onTapGesture { myVar += 1 }
TestChildView(myVar: $myVar) // Add `$` to pass the updated value.
}
}
}
struct TestChildView: View {
#Binding var myVar: Int // Create the value to be `#Binding`.
struct Item: Identifiable {
var id: String { return val }
let val: String
}
var list: [Item] {
return [Item(val: "a \(myVar)"), Item(val: "b \(myVar)"), Item(val: "c \(myVar)")]
}
var body: some View {
VStack {
List(list) { elem in
Text(elem.val)
}
.refreshable { print("myVar: \(myVar)") }
}
}
}
Roland's answer is correct. Use a binding so that the correct myVar value is used.
As to why: .refreshable, along with other stateful modifiers like .task, .onAppear, .onReceive, etc, operate on a different phase in the SwiftUI View lifecycle. You are correct in assuming that the closure passed to refreshable is stored in the environment and doesn't get updated as the views are recreated. This is intentional. It would make little sense to recreate this closure whenever the view is updated, because updating the view is kind of its intended goal.
You can think of .refreshable (and the other modifiers mentioned above) as similar to the #State and #StateObject property wrappers, in that they are persisted across view layout updates. A #Binding property can also be considered stateful because it is a two-way 'binding' to a state variable from a parent view.
In fact generally speaking, the closures you pass to .refreshable or .task should only read and write to stateful properties (such as viewModels) for this exact reason.
Simple sample code with toggle button (slightly modified from hackingwithswift:
This code(hackingwithswift original and my version) IS redrawing every list cell whenever any toggle happens. I modified code to better debug view drawing.
import SwiftUI
struct User: Identifiable {
let id = UUID()
var name: String
var isContacted = false
}
struct ProfileView: View {
#State private var users = [
User(name: "Taylor"),
User(name: "Justin"),
User(name: "Adele")
]
var body: some View {
let _ = Self._printChanges()
List($users) { $user in
ProfileCell(user: $user)
}
}
}
struct ProfileCell: View{
#Binding var user: User
var body: some View{
let _ = Self._printChanges()
Text(user.name)
Spacer()
Toggle("User has been contacted", isOn: $user.isContacted)
.labelsHidden()
}
}
Running app and toggling will print following in console for every toggle:
ProfileView: _users changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
Hackingwithswift tutorial states "Using a binding in this way is the most efficient way of modifying the list, because it won’t cause the entire view to reload when only a single item changes.", however that does not seem to be true.
Is it possible to redraw only item that was changed?
Theoretically it should be working, but it seems they changed something since first introduction, because now on state change they recreate(!) bindings (all of them), so automatic view changes handler interpret that as view update (binding is a property after all).
A possible workaround for this is to help rendering engine and check view equitability manually.
Tested with Xcode 13.4 / iOS 15.5
Main parts:
// 1
List($users) { $user in
EquatableView(content: ProfileCell(user: $user)) // << here !!
}
// 2
struct ProfileCell: View, Equatable {
static func == (lhs: ProfileCell, rhs: ProfileCell) -> Bool {
lhs.user == rhs.user
}
// ...
// 3
struct User: Identifiable, Equatable {
Test module is here
I'm doing a comparison of Core Data and Realm in a SwiftUI app, and Core Data does something that I'm hoping to figure out how to do in Realm.
Core Data lets you mutate objects whenever you want, and when they are ObservableObject in SwiftUI, your UI instantly updates. You then save the context whenever you want to persist the changes.
In Realm, the objects in the UI are live, but you can't change them unless you are in a write transaction. I'm trying to get my UI to reflect live/instant changes from the user when the actual write is only performed occasionally. Below is a sample app.
Here's my Realm model:
import RealmSwift
class Item: Object, ObjectKeyIdentifiable{
#objc dynamic var recordName = ""
#objc dynamic var text = ""
override class func primaryKey() -> String? {
return "recordName"
}
}
Here is my view model that also includes my save() function that only saves every 3 seconds. In my actual app, this is because it's an expensive operation and doing it as the user types brings the app to a crawl.
class ViewModel: ObservableObject{
static let shared = ViewModel()
#Published var items: Results<Item>!
#Published var selectedItem: Item?
var token: NotificationToken? = nil
init(){
//Add dummy data
let realm = try! Realm()
realm.beginWrite()
let item1 = Item()
item1.recordName = "one"
item1.text = "One"
realm.add(item1, update: .all)
let item2 = Item()
item2.recordName = "two"
item2.text = "Two"
realm.add(item2, update: .all)
try! realm.commitWrite()
self.fetch()
//Notifications
token = realm.objects(Item.self).observe{ [weak self] _ in
self?.fetch()
}
}
//Get Data
func fetch(){
let realm = try! Realm()
self.items = realm.objects(Item.self)
}
//Save Data
var saveTimer: Timer?
func save(item: Item, text: String){
//Save occasionally
saveTimer?.invalidate()
saveTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false){ _ in
let realm = try! Realm()
try? realm.write({
item.text = text
})
}
}
}
Last of all, here is the UI. It's pretty basic and reflects the general structure of my app where I'm trying to pull this off.
struct ContentView: View {
#StateObject var model = ViewModel.shared
var body: some View {
VStack{
ForEach(model.items){ item in
HStack{
Button(item.text){
model.selectedItem = item
}
Divider()
ItemDetail(item: item)
}
}
}
}
}
...and the ItemDetail view:
struct ItemDetail: View{
#ObservedObject var item: Item
#StateObject var model = ViewModel.shared
init(item: Item){
self.item = item
}
var body: some View{
//Binding
let text = Binding<String>(
get: { item.text },
set: { model.save(item: item, text: $0) }
)
TextField("Text...", text: text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
When I type in the TextField, how do I get the Button text to reflect what I have typed in real time considering that my realm.write only happens every 3 seconds? My Button updates after a write, but I want the UI to respond live--independent of the write.
Based on the suggested documentation from Jay, I got the following to work which is quite a bit simpler:
My main view adds the #ObservedResults property wrapper like this:
struct ContentView: View {
#StateObject var model = ViewModel.shared
#ObservedResults(Item.self) var items
var body: some View {
VStack{
ForEach(items){ item in
HStack{
Button(item.text){
model.selectedItem = item
}
Divider()
ItemDetail(item: item)
}
}
}
}
}
...and then the ItemDetail view simply uses an #ObservedRealmObject property wrapper that binds to the value in Realm and manages the writes automatically:
struct ItemDetail: View{
#ObservedRealmObject var item: Item
var body: some View{
TextField("Text...", text: $item.text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
This is essentially how Core Data does it (in terms view code) except Realm saves to the store automatically. Thank you, Jay!
I have a view and a viewModel that should update the ListView when users are added to the user array. I can verify that users are being added, yet the ObservedObject is not updating.
I have a search bar that lets you search users and then updates user array in the ViewModel which is supposed to update the View but it doesn't.
ViewModel
class UsersViewModel: ObservableObject {
#Published var users: [User] = []
#Published var isLoading = false
var searchText: String = ""
func searchTextDidChange() {
isLoading = true
API.User.searchUser(text: searchText) { (users) in
self.isLoading = false
self.users = users
}
// confirmed that users has data now at this point
}
}
View
struct UsersView: View {
#ObservedObject var usersViewModel = UsersViewModel()
var body: some View {
VStack() {
SearchBarView(text: $usersViewModel.searchText, onSearchButtonChanged: usersViewModel.searchTextDidChange)
// Not getting called after usersViewModel.users gets data
if (usersViewModel.users.count > 0) {
Text(usersViewModel.users[0].username)
}
}
}
}
You are likely winding up with different UsersViewModel objects:
#ObservedObject var usersViewModel = UsersViewModel()
Since UsersView is a struct, this creates a new model every time the View is instantiated (which can happen very often, not just when the view appears). In iOS 14 there is #StateObject to combine State (which preserves information between View instantiations) with ObservedObject, but in iOS 13 I recommend passing in the ObservedObject if it's not a shared instance.
Try to update on main queue
API.User.searchUser(text: searchText) { (users) in
DispatchQueue.main.async {
self.isLoading = false
self.users = users
}
}
If your view is inside another view and you are not injecting the view model, consider using #StateObject.
This will not cause the object to be renewed every time the view is re-rendered.
I'm new to SwiftUI and understand that I may need to implement EnvironmentObject in some way, but I'm not sure how in this case.
This is the Trade class
class Trade {
var teamsSelected: [Team]
init(teamsSelected: [Team]) {
self.teamsSelected = teamsSelected
}
}
This is the child view. It has an instance trade from the Trade class. There is a button that appends 1 to array teamsSelected.
struct TeamRow: View {
var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
This is the parent view. As you can see, I pass trade into the child view TeamRow. I want trade to be in sync with trade in TeamRow so that I can then pass trade.teamsSelected to TradeView.
struct TeamSelectView: View {
var trade = Trade(teamsSelected: [])
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(teamsSelected: trade.teamsSelected)) {
Text("Trade")
}
List {
ForEach(teams) { team in
TeamRow(trade: self.trade)
}
}
}
}
}
}
I've taken your code and changed some things to illustrate how SwiftUI works in order to give you a better understanding of how to use ObservableObject, #ObservedObject, #State, and #Binding.
One thing to mention up front - #ObservedObject is currently broken when trying to run SwiftUI code on a physical device running iOS 13 Beta 6, 7, or 8. I answered a question here that goes into that in more detail and explains how to use #EnvironmentObject as a workaround.
Let's first take a look at Trade. Since you're looking to pass a Trade object between views, change properties on that Trade object, and then have those changes reflected in every view that uses that Trade object, you'll want to make Trade an ObservableObject. I've added an extra property to your Trade class purely for illustrative purposes that I'll explain later. I'm going to show you two ways to write an ObservableObject - the verbose way first to see how it works, and then the concise way.
import SwiftUI
import Combine
class Trade: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var name: String {
willSet {
self.objectWillChange.send()
}
}
var teamsSelected: [Int] {
willSet {
self.objectWillChange.send()
}
}
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
When we conform to ObservableObject, we have the option to write our own ObservableObjectPublisher, which I've done by importing Combine and creating a PassthroughSubject. Then, when I want to publish that my object is about to change, I can call self.objectWillChange.send() as I have on willSet for name and teamsSelected.
This code can be shortened significantly, however. ObservableObject automatically synthesizes an object publisher, so we don't actually have to declare it ourselves. We can also use #Published to declare our properties that should send a publisher event instead of using self.objectWillChange.send() in willSet.
import SwiftUI
class Trade: ObservableObject {
#Published var name: String
#Published var teamsSelected: [Int]
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
Now let's take a look at your TeamSelectView, TeamRow, and TradeView. Keep in mind once again that I've made some changes (and added an example TradeView) just to illustrate a couple of things.
struct TeamSelectView: View {
#ObservedObject var trade = Trade(name: "Name", teamsSelected: [])
#State var teams = [1, 1, 1, 1, 1]
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(trade: self.trade)) {
Text(self.trade.name)
}
List {
ForEach(self.teams, id: \.self) { team in
TeamRow(trade: self.trade)
}
}
Text("\(self.trade.teamsSelected.count)")
}
.navigationBarItems(trailing: Button("+", action: {
self.teams.append(1)
}))
}
}
}
struct TeamRow: View {
#ObservedObject var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
struct TradeView: View {
#ObservedObject var trade: Trade
var body: some View {
VStack {
Text("\(self.trade.teamsSelected.count)")
TextField("Trade Name", text: self.$trade.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}
Let's first look at #State var teams. We use #State for simple value types - Int, String, basic structs - or collections of simple value types. #ObservedObject is used for objects that conform to ObservableObject, which we use for data structures that are more complex than just Int or String.
What you'll notice with #State var teams is that I've added a navigation bar item that will append a new item to the teams array when pressed, and since our List is generated by iterating through that teams array, our view re-renders and adds a new item to our List whenever the button is pressed. That's a very basic example of how you would use #State.
Next, we have our #ObservedObject var trade. You'll notice that I'm not really doing anything different than you were originally. I'm still creating an instance of my Trade class and passing that instance between my views. But since it's now an ObservableObject, and we're using #ObservedObject, our views will now all receive publisher events whenever the Trade object changes and will automatically re-render their views to reflect those changes.
The last thing I want to point out is the TextField I created in TradeView to update the Trade object's name property.
TextField("Trade Name", text: self.$trade.name)
The $ character indicates that I'm passing a binding to the text field. This means that any changes TextField makes to name will be reflected in my Trade object. You can do the same thing yourself by declaring #Binding properties that allow you to pass bindings between views when you are trying to sync state between your views without passing entire objects.
While I changed your TradeView to take #ObservedObject var trade, you could simply pass teamsSelected to your trade view as a binding like this - TradeView(teamsSelected: self.$trade.teamsSelected) - as long as your TradeView accepts a binding. To configure your TradeView to accept a binding, all you would have to do is declare your teamsSelected property in TradeView like this:
#Binding var teamsSelected: [Team]
And lastly, if you run into issues with using #ObservedObject on a physical device, you can refer to my answer here for an explanation of how to use #EnvironmentObject as a workaround.
You can use #Binding and #State / #Published in Combine.
In other words, use a #Binding property in Child View and bind it with a #State or a #Published property in Parent View as following.
struct ChildView: View {
#Binding var property1: String
var body: some View {
VStack(alignment: .leading) {
TextField(placeholderTitle, text: $property1)
}
}
}
struct PrimaryTextField_Previews: PreviewProvider {
static var previews: some View {
PrimaryTextField(value: .constant(""))
}
}
struct ParentView: View{
#State linkedProperty: String = ""
//...
ChildView(property1: $linkedProperty)
//...
}
or if you have a #Publilshed property in your viewModel(#ObservedObject), then use it to bind the state like ChildView(property1: $viewModel.publishedProperty).
firstly thanks a lot to graycampbell for giving me a better understanding! However, my understanding does not seem to work out completely. I have a slightly different case which I'm not fully able to solve.
I've already asked my question in a separate thread, but I want to add it here as well, because it somehow fits the topic: Reading values from list of toggles in SwiftUI
Maybe somebody of you guys can help me with this. The main difference to the initial post if this topic is, that I have to collect Data from each GameGenerationRow in the GameGenerationView and then hand it over to another View.