SwiftUI sheet shows sheet with wrong data - swift

I have a list that displays 1, 2, and 3. When the text is tapped the app opens a sheet with the number tapped. However, if I tap the text in the second or third row, the number displayed in the sheet is still 1. What am I doing wrong?
import SwiftUI
struct ContentView: View {
var numbers = [1, 2, 3]
#State private var shouldPresentSheet = false
var body: some View {
List(self.numbers, id: \.self) { number in
Text("number: \(number)").sheet(isPresented: self.$shouldPresentSheet) {
Text("This is sheet number \(number)")
}.onTapGesture {
self.shouldPresentSheet.toggle()
}
}
}
}

You need to create one sheet instead of multiple sheets.
If you want to use .sheet(isPresented:content:) you can do the following:
struct ContentView: View {
var numbers = [1, 2, 3]
#State private var selectedNumber: Int?
var body: some View {
List(numbers, id: \.self) { number in
Text("number: \(number)")
.onTapGesture {
self.selectedNumber = number
}
}
.sheet(isPresented: binding) {
Text("This is a sheet number \(self.selectedNumber ?? 0)")
}
}
var binding: Binding<Bool> {
.init(
get: { self.selectedNumber != nil },
set: { _ in }
)
}
}
Alternatively you can use .sheet(item:content:):
struct Model: Identifiable {
let id: Int
}
struct ContentView: View {
var numbers = [1, 2, 3].map(Model.init)
#State private var selectedNumber: Model?
var body: some View {
List(numbers, id: \.id) { number in
Text("number: \(number.id)")
.onTapGesture {
self.selectedNumber = number
}
}
.sheet(item: $selectedNumber) { item in
Text("This is a sheet number \(item.id)")
}
}
}
EDIT
If you decide to use .sheet(item:content:) you can add an extension to Int (instead of creating a custom struct conforming to Identifiable):
extension Int: Identifiable {
public var id: Int { self }
}
as proposed in Asperi's answer.

Related

SwiftUI: How to update element in ForEach without necessity to update all elements?

Imagine that you have some parent view that generate some number of child views:
struct CustomParent: View {
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index)
}
}
}
}
struct CustomChild: View {
#State var index: Int
#State private var text: String = ""
var body: some View {
Button(action: {
// Here should be some update of background/text/opacity or whatever.
// So how can I update background/text/opacity or whatever for button with index for example 3 from button with index for example 1?
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Question is included in the code as comment.
Thanks!
UPDATE:
First of all really thanks for all of your answers, but now imagine that you use mentioned advanced approach.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
If I use let _ = Self._printChanges() method in CustomChildView, to catch UI updates/changes, it'll print that every element in ForEach was updated/changed on button action.
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
let _ = Self._printChanges() // This have been added to code
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}
And now imagine that you have for example 1000 custom elements which have some background, opacity, shadow, texts, fonts and so on. Now I change text in any of the elements.
Based on log from let _ = Self._printChanges() method, it goes through all elements, and all elements are updated/changed what can cause delay.
Q1: Why did update/change all elements, if I change text in only one element?
Q2: How can I prevent update/change all elements, if I change only one?
Q3: How to update element in ForEach without necessity to update all elements?
Simpler Approach:
Although child views cannot access things that the host views have, it's possible to declare the child states in the host view and pass that state as a binding variable to the child view. In the code below, I have passed the childTexts variable to the child view, and (for your convenience) initialized the text so that it binds to the original element in the array (so that your onAppear works properly). Every change performed on the text and childTexts variable inside the child view reflects on the host view.
I strongly suggest not to do this though, as more elegant approaches exist.
struct CustomParent: View {
#State var childTexts = [String](repeating: "", count: 10)
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, childTexts: $childTexts)
}
}
}
}
struct CustomChild: View {
let index: Int
#Binding private var text: String
#Binding private var childTexts: [String]
init(index: Int, childTexts: Binding<[String]>) {
self.index = index
self._childTexts = childTexts
self._text = childTexts[index]
}
var body: some View {
Button(action: {
//button behaviors goes here
//for example
childTexts[index + 1] = "A"
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Advanced Approach:
By using the Combine framework, all your logics can be moved into an ObservableObject view model. This is much better as the button logic is no longer inside the view. In simplest terms, the #Published variable in the ObservableObject will publish a change when it senses its own mutation, while the #StateObjectand the #ObservedObject will listen and recalculate the view for you.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}

Swift ForEach with non constant range View Refresh

I know it's a simple question, but I haven't found an answer. I want to understand the underlying concept.
I'm trying to update a ForEach with a non constant range, the closing parameter is a variable that is assigned to a button.
The variable is assigned with a #State so it's supposed to refresh the view. Somehow it's not working.
import SwiftUI
struct ContentView: View {
#State private var numberOfTimes = 5
let timesPicker = [2,5,10,12,20]
#State private var tableToPractice = 2
enum answerState {
case unanswered
case wrong
case right
}
func listRange(){
}
var body: some View {
NavigationView{
HStack{
VStack{
Form{
Section {
Picker("Tip percentage", selection: $numberOfTimes) {
ForEach(timesPicker, id: \.self) {
Text($0, format: .number)
}
}
.pickerStyle(.segmented)
} header: {
Text("How many times do you want to practice?")
}
Section{
Stepper("Table to practice: \(tableToPractice.formatted())", value: $tableToPractice, in: 2...16 )
}
Button("Start Now", action: listRange).buttonStyle(.bordered)
List{
ForEach(0..<numberOfTimes){
Text("Dynamic row \($0)")
}
}
}.foregroundColor(.gray)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The problem is that the range is not identified. Lets make some rows
struct Row: Identifiable {
let id = UUID()
}
Then set up an array of identifiable items
#State private var numberOfTimes = 5
#State private var rows = Array(repeating: Row(), count: 5)
Now you can have a responsive list
List{
ForEach(rows) { row in
Text("Dynamic row")
}
}
Call the on change update to recreate the array
.onChange(of: numberOfTimes) { newValue in
rows = Array(repeating: Row(), count: newValue)
numberOfTimes = newValue
}
the onChange should be called on the Form.
This will make more sense when you have better view model data, see apple documentation for a more in depth example.
This is for lazy v stack, but the data model setup is what I'm thinking of
https://developer.apple.com/documentation/swiftui/grouping-data-with-lazy-stack-views

swiftui: detail view uses false List style (that of its parent) before it switches to the correct style (with delay)

I have a master-detail view with three levels. At the first level, a person is selected. At the second level, the persons' properties are shown using a grouped list with list style "InsetGroupedListStyle()"
My problem is: each time, the third level (here called "DetailView()") is displayed, it is displayed with the wrong style (the style of its parent), before it switches to the correct style with some delay.
This is a bad user experience. Any ideas?
Thanks!
import SwiftUI
struct Person: Identifiable, Hashable {
let id: Int
let name: String
}
class Data : ObservableObject {
#Published var persons: [Person] = [
Person(id: 0, name: "Alice"),
Person(id: 1, name: "Bob"),
]
}
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var data = Data()
var body: some View {
NavigationView {
List {
ForEach(data.persons, id: \.self) { person in
NavigationLink(
destination: EditPerson(data: data, psId: person.id),
label: {
Text(person.name)
})
}
}.environment(\.defaultMinListRowHeight, 10)
.navigationTitle("Persons")
}
}
}
struct EditPerson: View {
#ObservedObject var data: Data
var psId: Int
var body: some View {
List() {
Section(header:
Text("HEADER1 ")
) {
NavigationLink(
destination: DetailView(data: data),
label: {
Text("1st link")
}
)
}
}.navigationTitle("Person #" + String(psId))
.listStyle(InsetGroupedListStyle()) // <--- the style specified here
// is preliminarily used for the DetailView, too.
}
}
struct DetailView: View {
#ObservedObject var data : Data
var body: some View {
List { // <- this list is displayed with grouped list style before
// it is updated some split seconds later
Button(action: {
print("button1 pressed")
}) {
Text("Button1")
}
Button(action: {
print("button2 pressed")
}) {
Text("Button2")
}
}
}
}
It seems to work if you simply explicitly set it back to PlainListStyle in DetailView():
struct DetailView: View {
#ObservedObject var data : Data
var body: some View {
List { // <- this list is displayed with grouped list style before
// it is updated some split seconds later
Button(action: {
print("button1 pressed")
}) {
Text("Button1")
}
Button(action: {
print("button2 pressed")
}) {
Text("Button2")
}
}
.listStyle(PlainListStyle()) // Explicitly set it here
}
}

SwiftUI ForEach.onDelete doesn't work correctly in TabView

I have tried to implement a basic list with a ForEach loop. I've tried with Both a hashable struct and basic array Using ForEach(..., id: \.self) as shown below. Whenever it's inside a tab view and changing to EditMode.active, you have to press the - for about a sec as if it's a LongPressGesture() until it will show the swipe to delete.
Example of problem
struct HashArray: Hashable {
let id = UUID()
init(number: Int) {
self.number = number
}
let number: Int
}
struct TabStuff: View {
#State var markers: [Int] = [1, 2, 3, 4, 5]
#State var testArray: [HashArray] = [HashArray(number: 0), HashArray(number: 1), HashArray(number: 2), HashArray(number: 3)]
var body: some View {
VStack {
EditButton()
List {
ForEach(testArray, id: \.id) { marker in
Text("Number: \(marker.number)")
}.onDelete(perform: { indexSet in
markers.remove(atOffsets: indexSet)
})
}
}
}
}
struct OtherView: View {
#State var markers: [Int] = [1, 2, 3, 4, 5]
var body: some View {
VStack {
EditButton()
List {
ForEach(markers, id: \.self) { marker in
Text("\(marker)")
}.onDelete(perform: { indexSet in
markers.remove(atOffsets: indexSet)
})
}
}
}
}
both of which don't work when putting inside of a tab view like so
struct ContentView: View {
#State var selectedTab: NavigationTab = .list
var body: some View {
TabView(selection: $selectedTab) {
TabStuff()
.onTapGesture {
selectedTab = .list
}
.tabItem {
Text("List Test")
}
OtherView()
.onTapGesture {
selectedTab = .secondView
}.tabItem {
Text("Second View")
}
}
}
}
Any ideas on how to get it to react appropriately?
It's because you have an .onTapGesture() on the entire view that is taking priority when a user taps anywhere in your view.
The tab items will automatically let you switch between tabs without having to update the selection, so you can just remove the tap gesture entirely. However, if you need to update the selectedTab when switching, you can use a .tag() modifier on the .tabItem.
TabStuff()
// REMOVE THIS
//.onTapGesture {
// selectedTab = .list
//}
.tabItem {
Text("List Test")
.tag(NavigationTab.list) // ADD THIS
}

Load different data by ID in the same View

I want to load different data in the same View using an ObservableObject with an Index.
As you can see in the simple demo example attached when I go through the views changing the id in order to load different data it works.
But when in any point I change another data from another observable object (in this case AnotherObservable) then my view breaks and any data is found at all.
Navigation works...
Once I press Change Title button...
To test de issue just simple copy and paste the code and navigate through the data with the <- -> and then press Change title button
class DemoItemsLoader: ObservableObject {
#Published var items = [1, 2, 3, 4]
}
class DemoArticleLoader: ObservableObject {
#Published var items = [1 : "One", 2 : "Two" , 3 : "Three", 4 : "Four"]
#Published var realData: String?
func loadItemForID(id: Int) {
realData = items[id] ?? "Not found"
}
}
class AnotherObservable: ObservableObject {
#Published var title: String = "Title"
}
struct Class1: View {
#StateObject var anotherObservable = AnotherObservable()
#StateObject var itemsLoader = DemoItemsLoader()
var body: some View {
NavigationView{
VStack {
ForEach(itemsLoader.items, id: \.self) { item in
NavigationLink(
destination: Class2(anotherObservable: anotherObservable, articleLoader: DemoArticleLoader(), id: item)) {
Text("\(item)")
}
}
}
}
}
}
struct Class2: View {
#ObservedObject var anotherObservable: AnotherObservable
#ObservedObject var articleLoader: DemoArticleLoader
#State var id: Int
var body: some View {
VStack {
Text(anotherObservable.title)
Text(articleLoader.realData ?? "Not found")
HStack {
Spacer()
Button(action: {
if id > 1 {
id = id - 1
articleLoader.loadItemForID(id: id)
}
}) {
Text("<-")
}
Button(action: {
if id < articleLoader.items.count {
id = id + 1
articleLoader.loadItemForID(id: id)
}
}) {
Text("->")
}
Button(action: {
anotherObservable.title = "Another Title"
}) {
Text("Change title")
}
Spacer()
}
}
.onAppear { articleLoader.loadItemForID(id: id)}
}
}
The issue is that you're calling loadItemForID in onAppear only:
.onAppear { articleLoader.loadItemForID(id: id)}
And when you change title, the above function is not called again.
The easiest solution is probably to store DemoArticleLoader() as a #StateObject, so it's not recreated every time:
struct Class1: View {
#StateObject var anotherObservable = AnotherObservable()
#StateObject var itemsLoader = DemoItemsLoader()
#StateObject var articleLoader = DemoArticleLoader() // create once
var body: some View {
NavigationView {
VStack {
ForEach(itemsLoader.items, id: \.self) { item in
NavigationLink(
// pass here
destination: Class2(anotherObservable: anotherObservable, articleLoader: articleLoader, id: item)) {
Text("\(item)")
}
}
}
}
}
}