Passing data to .popover in swiftui - swift

I have a String value that changes after pressing the button, and I have a .popover to show it's value, but the value is not changing. I've added print() on each step to clearly show that the value had changed
My ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var showingPopover = false
#State private var popoverText = "hey, I'm the initial text"
var body: some View {
Button("press me!") {
print("1) \(popoverText)")
popoverText = "hey, I'm the new text"
print("2) \(popoverText)")
showingPopover = true
print("3) \(popoverText)")
}
.popover(isPresented: $showingPopover) {
Text(popoverText)
.onAppear(perform: {print("4) \(popoverText)")})
}
}
}
After pressing the button it prints out this:
1) hey, I'm the initial text
2) hey, I'm the new text
3) hey, I'm the new text
4) hey, I'm the new text
And shows this:
Though I've found a weird workaround. Everything works fine if I add Text(popoverText):
import SwiftUI
struct ContentView: View {
#State private var showingPopover = false
#State private var popoverText = "hey, I'm the initial text"
var body: some View {
VStack{
Text(popoverText)
Button("press me!") {
print("1) \(popoverText)")
popoverText = "hey, I'm the new text"
print("2) \(popoverText)")
showingPopover = true
print("3) \(popoverText)")
}
}
.popover(isPresented: $showingPopover) {
Text(popoverText)
.onAppear(perform: {print("4) \(popoverText)")})
}
}
}
It prints the same thing:
1) hey, I'm the initial text
2) hey, I'm the new text
3) hey, I'm the new text
4) hey, I'm the new text
But now it shows the new text:
I think it has something to do with view refreshing or focus but I had no success in nailing down the exact thing
EDIT: my actual application is not with the button, code below is just made as an example so keeping .isPresented is necessary, and it's only allowed to use either .isPresented or .item

.popover(item: someIdentifiableObj, content: ...) is use to pass data into popover
struct HomeView: View {
#State var userObj: User?
var body: some View {
Button("press me!") {
userObj = User(popoverText: "New Text")
}
.popover(item: $userObj) { user in
Text(user.popoverText)
}
}
}
// MARK: - PREVIEW
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
struct User: Identifiable {
let id: UUID = UUID()
let popoverText: String
}

Related

TextField's .focused modifier not working when "out of view" SwiftUI

I'm working with a SwiftUI List with a TextEditor at the bottom of the List. The TextEditor becomes active or "Focused" when a button is pressed.
To achieve this, I have added a .focused modifier to the TextEditor. When the List is scrolled to the bottom and the TextEditor is visible this works as expected and shows the keyboard when the button is pressed.
But the problem is that when the List is scrolled to the top and the TextEditor is not visible on the screen the TextField doesn't seem to get focused.
import SwiftUI
struct ContentView: View {
var array = Array(0...20)
#State private var newItemText : String = ""
#FocusState var focused: Bool
var body: some View {
VStack{
List{
ForEach(array, id: \.self) {
Text("\($0)")
}
TextEditor(text: $newItemText)
.focused($focused)
}
Button {
focused = true
} label: {
Text("Add Item")
}
}
}
}
You have to set the field in a DispatchQueue.
import SwiftUI
struct ContentView: View {
var array = Array(0...20)
#State private var newItemText : String = ""
#FocusState var focusedField: Bool
var body: some View {
VStack {
ScrollViewReader { scrollView in
List {
ForEach(array, id: \.self) {
Text("\($0)")
}
TextField("", text: self.$newItemText)
.id("TextEditor")
.focused($focusedField)
}
Button(action: {
scrollView.scrollTo("TextEditor")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.focusedField = true
}
}) {
Text("Add Item")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Hope this solves your problem!

Table selection issue

I'm having this weird issue with the new Table introduced in Xcode 13, where I cannot programmatically select the first row. (But other rows work fine)
Reproduction:
Make sure that macOS Montery and Xcode 13 Beta 2 are installed
Paste the following code in a new blank SwiftUI project, with its deployment target set to 12.0 (macOS Montery):
struct ContentView: View {
#State var data: [TestStruct] = []
#State var selection = Set<UUID>()
var body: some View {
Table(data, selection: $selection) {
TableColumn("column", value: \.text)
}
.toolbar {
Button(action: {
data.append(TestStruct(text: "Test"))
selection = Set([data.last!.id])
}) {
Text("Add")
}
}
}
}
struct TestStruct: Identifiable {
var text: String
var id = UUID()
}
Run the app, and press the "Add" button
Observe that no selection takes place, but when you press it again, a selection should appear
How would you fix this issue? Or is this a bug on Apple's part?
try something like this:
struct ContentView: View {
#State var data: [TestStruct] = []
// #State var selection = Set<UUID>() // <--- for multiple selections
#State var selection: UUID? = nil // <--- if you want just 1 selection
var body: some View {
Table(data, selection: $selection) {
TableColumn("column", value: \.text)
}
.toolbar {
Button(action: {
data.append(TestStruct(text: "Test"))
if let last = data.last {
DispatchQueue.main.async {
// selection.insert(last.id)
selection = last.id
}
}
}) {
Text("Add")
}
}
}
}

How to Generate a New Random Number Once Every time Content View Loads?

The intent here is generate a new random number every time MyView loads while keeping that randomly generated number unaffected with any MyView refresh. However, none of the approaches below work. I will explain each approach and its outcome. Any ideas on how to properly accomplish what I am asking?
Approach #1: I assumed a new number is generated every time MyView loads. However, a random number is generated once for the entire app lifetime. I placed a button to force a view refresh, so when I press the button a new random number should not generate, which is what happens. What is wrong with this approach?
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: MyView()) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#State var randomInt = Int.random(in: 1...100)
#State var myMessage: String = ""
var body: some View {
Text(String(randomInt))
Button("Press Me") {
myMessage = String(randomInt)
}
Text(myMessage)
}
}
Approach #2: I tried to update randomInt variable via let inside the Body but this generates a new random number every time the button is pressed (which is forcing a view refresh). Again, not the intended outcome. What is wrong with this approach?
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: MyView()) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#State var randomInt = 0
#State var myMessage: String = ""
var body: some View {
let randomInt = Int.random(in: 1...100)
Text(String(randomInt))
Button("Press Me") {
myMessage = String(randomInt)
}
Text(myMessage)
}
}
Approach #3: The idea here to pass a new randomly generated integer every time the "Link to MyView" is pressed. I assumed that Int.Random is ran and passed every time Content View loads. However, a random number is only generated the first-time the entire app runs. What is wrong with this approach?
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: MyView(randomInt: Int.random(in: 1...100))) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#State var randomInt = 0
#State var myMessage: String = ""
var body: some View {
Text(myMessage)
Button("Press Me") {
myMessage = String(randomInt)
}
}
}
Approach #1:
MyView is created once, when ContentView renders.
Inside MyView, randomInt is set when the view is first created and then never modified. When you press the button, myMessage is set, but randomInt is never changed. If you wanted randomInt to change, you'd want to say something like:
randomInt = Int.random(in: 1...100)
inside your button action:
Approach #2:
You're creating a new randomInt variable in the local scope by declaring let randomInt = inside the view body.
Instead, if I'm reading your initial question correctly, I think you'd want something using onAppear:
struct MyView: View {
#State var randomInt = 0
#State var myMessage: String = ""
var body: some View {
VStack {
Text(String(randomInt))
Button("Press Me") {
myMessage = String(randomInt)
}
Text(myMessage)
}.onAppear {
randomInt = Int.random(in: 1...100)
}
}
}
You'll see that with this, every time you go back in the navigation hierarchy and then revisit MyView, there's a new value (since it appears again). The button triggering a re-render doesn't re-trigger onAppear
Approach #3:
MyView gets created on the first render of the parent view (ContentView). Unless something triggers a refresh of ContentView, you wouldn't generate a new random number here.
In conclusion, I'm a little unclear on what the initial requirement is (what does it mean for a View to 'load'? Does this just mean when it gets shown on the screen?), but hopefully this describes each scenario and maybe introduces the idea of onAppear, which seems like it might be what you're looking for.
Addendum: if you want the random number to be generated only when ContentView loads (as the title says), I'd create it in ContentView instead of your MyView and pass it as a parameter.
struct ContentView: View {
#State var randomInt = Int.random(in: 1...100)
var body: some View {
NavigationView {
NavigationLink(destination: MyView(randomInt: $randomInt)) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#Binding var randomInt : Int
#State var myMessage: String = ""
var body: some View {
Text(String(randomInt))
Button("Press me to refresh only myMessage") {
myMessage = String(randomInt)
}
Button("Press me to change the randomInt as well") {
randomInt = Int.random(in: 1...100)
myMessage = String(randomInt)
}
Text(myMessage)
}
}
I work on a way that always make a new random, your problem was you just used initialized State over and over! Without changing it.
Maybe the most important thing that you miss understood was using a State wrapper and expecting it performance as computed property, as you can see in your 3 example all has the same issue in different name or form! So for your information computed property are not supported in SwiftUI until Now! That means our code or us should make an update to State Wrappers, and accessing them like you tried has not meaning at all to them.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: MyView()) {
Text("Link to MyView")
}
}
}
}
struct MyView: View {
#State private var randomInt: Int?
var body: some View {
if let unwrappedRandomInt: Int = randomInt { Text(unwrappedRandomInt.description) }
Button("Press Me") { randomMakerFunction() }
.onAppear() { randomMakerFunction() }
}
func randomMakerFunction() { randomInt = Int.random(in: 1...100) }
}

How to correctly handle Picker in Update Views (SwiftUI)

I'm quite new to SwiftUI and I'm wondering how I should use a picker in an update view correctly.
At the moment I have a form and load the data in with .onAppear(). That works fine but when I try to pick something and go back to the update view the .onAppear() gets called again and I loose the picked value.
In the code it looks like this:
import SwiftUI
struct MaterialUpdateView: View {
// Bindings
#State var material: Material
// Form Values
#State var selectedUnit = ""
var body: some View {
VStack(){
List() {
Section(header: Text("MATERIAL")){
// Picker for the Unit
Picker(selection: $selectedUnit, label: Text("Einheit")) {
ForEach(API().units) { unit in
Text("\(unit.name)").tag(unit.name)
}
}
}
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Does anyone has experience with that problem or am I doing something terribly wrong?
You need to create a custom binding which we will implement in another subview. This subview will be initialised with the binding vars selectedUnit and material
First, make your MaterialUpdateView:
struct MaterialUpdateView: View {
// Bindings
#State var material : Material
// Form Values
#State var selectedUnit = ""
var body: some View {
NavigationView {
VStack(){
List() {
Section(header: Text("MATERIAL")) {
MaterialPickerView(selectedUnit: $selectedUnit, material: $material)
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Then, below, add your MaterialPickerView, as shown:
Disclaimer: You need to be able to access your API() from here, so move it or add it in this view. As I have seen that you are re-instanciating it everytime, maybe it is better that you store its instance with let api = API() and then refer to it with api, and even pass it to this view as such!
struct MaterialPickerView: View {
#Binding var selectedUnit: String
#Binding var material : Material
#State var idx: Int = 0
var body: some View {
let binding = Binding<Int>(
get: { self.idx },
set: {
self.idx = $0
self.selectedUnit = API().units[self.idx].name
self.material.unit = self.selectedUnit
})
return Picker(selection: binding, label: Text("Einheit")) {
ForEach(API().units.indices) { i in
Text(API().units[i].name).tag(API().units[i].name)
}
}
}
}
That should do,let me know if it works!

SwiftUI Navigation: How to switch detail view to a different item?

I'm struggling implementing the following navigation behavior:
From a list the user can select an item which triggers a detail view for this item. On this detail view there is an "Add" button in the navigation bar which opens a modal sheet for adding an other item.
Up to this point, everything works as expected.
But after adding the item, I want the detail view to show the new item. I tried to set the list selection to the id of the new item. This triggers the detail view to disappear, the list selects the new item and show the details for a very short time, then the detail view disappears again and the list is shown.
I've tried adding a bridged binding and let the list view not set the selection to nil, this solves the issue at first, but then the "Back" button isn't working anymore.
Please note: I want the "Add" button on the detail view and not on the list view as you would expect it.
Here's the full code to test:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var state = AppState.shared
var body: some View {
NavigationView {
List(state.items) {item in
NavigationLink(destination: DetailView(item: item), tag: item.id, selection: self.$state.selectedId) {
Text(item.title)
}
}
.navigationBarTitle("Items")
}
}
}
struct DetailView: View {
var item: Item
#State private var showForm = false
var body: some View {
Text(item.title)
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
})
.sheet(isPresented: $showForm, content: { FormView() })
}
}
struct FormView: View {
#Environment(\.presentationMode) private var presentationMode
private var state = AppState.shared
var body: some View {
Button("Add") {
let id = self.state.items.count + 1
self.state.items.append(Item(id: id, title: "Item \(id)"))
self.presentationMode.wrappedValue.dismiss()
self.state.selectedId = id
}
}
}
class AppState: ObservableObject {
static var shared = AppState()
#Published var items: [Item] = [Item(id: 1, title: "Item 1")]
#Published var selectedId: Int?
}
struct Item: Identifiable {
var id: Int
var title: String
}
In your scenario it is needed to make navigation link destination independent, so it want be reactivated/invalidated when destination changed.
Here is possible approach. Tested with Xcode 11.7 / iOS 13.7
Updated code only:
struct ContentView: View {
#ObservedObject private var state = AppState.shared
#State private var isActive = false
var body: some View {
NavigationView {
List(state.items) {item in
HStack {
Button(item.title) {
self.state.selectedId = item.id
self.isActive = true
}
Spacer()
Image(systemName: "chevron.right").opacity(0.5)
}
}
.navigationBarTitle("Items")
.background(NavigationLink(destination: DetailView(), isActive: $isActive) { EmptyView() })
}
}
}
struct DetailView: View {
#ObservedObject private var state = AppState.shared
#State private var showForm = false
#State private var fix = UUID() // << fix for known issue with bar button misaligned after sheet
var body: some View {
Text(state.selectedId != nil ? state.items[state.selectedId! - 1].title : "")
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
}.id(fix))
.sheet(isPresented: $showForm, onDismiss: { self.fix = UUID() }, content: { FormView() })
}
}