Why is my Swiftui detail view not displaying the data from my tapped list item? [duplicate] - ios

Here is my example, and I can't tell if this is a bug or not. All my cells load correctly, but when I try to bring up the DetailView() as a sheet, the item pased in is always whatevr item is shown first in the grid (in the top left in my case here), NOT the "cell" that was tapped. So, why is the item from ForEach loop correctly populating the cells, but not being passed to the .sheet via the button?
import SwiftUI
let columnCount: Int = 11
let gridSpacing: CGFloat = 1
struct GridView: View {
#State var showingDetail = false
let data = (1...755).map { "\($0)" }
let columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: gridSpacing), count: columnCount)
let colCount: CGFloat = CGFloat(columnCount)
var body: some View {
GeometryReader { geo in
ScrollView (showsIndicators: false) {
LazyVGrid(columns: columns, spacing: gridSpacing) {
ForEach(data, id: \.self) { item in
Button(action: {
self.showingDetail.toggle()
}) {
GridCell(item: item, size: (geo.size.width - (colCount * gridSpacing)) / colCount)
}.sheet(isPresented: $showingDetail) {
DetailView(item: item)
}
}
}
}
.padding(.horizontal, gridSpacing)
}
}
}
struct GridCell: View {
let isVault: Bool = false
let item: String
let size: CGFloat
var body: some View {
ZStack {
Rectangle()
.fill(Color.orange)
.cornerRadius(4)
Text(item)
.foregroundColor(.white)
.font(.system(size: size * 0.55))
}
.frame(height: size)
}
}
struct DetailView: View {
let item: String
var body: some View {
Text(item)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
GridView()
.preferredColorScheme(.dark)
}
}
What am I missing? Also, scrolling on my iPad pro 11 is a bit jumpy, does anyone esle see the same behaviour?

In such use-case it is more appropriate to use variant of sheet constructed with item, because sheet must be moved out of dynamic content (otherwise you create as many sheets as items in ForEach).
Here is possible solution. Tested with Xcode 12 / iOS 14.
// helper extension because .sheet(item:...) requires item to be Identifiable
extension String: Identifiable {
public var id: String { self }
}
struct GridView: View {
#State private var selected: String? = nil
let data = (1...755).map { "\($0)" }
let columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: gridSpacing), count: columnCount)
let colCount: CGFloat = CGFloat(columnCount)
var body: some View {
GeometryReader { geo in
ScrollView (showsIndicators: false) {
LazyVGrid(columns: columns, spacing: gridSpacing) {
ForEach(data, id: \.self) { item in
Button(action: {
selected = item // store selected item
}) {
GridCell(item: item, size: (geo.size.width - (colCount * gridSpacing)) / colCount)
}
}
}
}.sheet(item: $selected) { item in // activated on selected item
DetailView(item: item)
}
.padding(.horizontal, gridSpacing)
}
}
}

Related

How to scroll through items in scroll view using keyboard arrows in SwiftUI?

I've built a view that has scroll view of horizontal type with HStack for macOS app. Is there a way to circle those items using keyboard arrows?
(I see that ListView has a default behavior but for other custom view types there are none)
click here to see the screenshot
var body: some View {
VStack {
ScrollView(.horizontal, {
HStack {
ForEach(items.indices, id: \.self) { index in
//custom view for default state and highlighted state
}
}
}
}
}
any help is appreciated :)
You could try this example code, using my previous post approach, but with a horizontal scrollview instead of a list. You will have to adjust the code to your particular app. My approach consists only of a few lines of code that monitors the key events.
import Foundation
import SwiftUI
import AppKit
struct ContentView: View {
let fruits = ["apples", "pears", "bananas", "apricot", "oranges"]
#State var selection: Int = 0
#State var keyMonitor: Any?
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
ForEach(fruits.indices, id: \.self) { index in
VStack {
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(10)
Text(fruits[index]).tag(index)
}
.background(selection == index ? Color.red : Color.clear)
.padding(10)
}
}
}
.onAppear {
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { nsevent in
if nsevent.keyCode == 124 { // arrow right
selection = selection < fruits.count ? selection + 1 : 0
} else {
if nsevent.keyCode == 123 { // arrow left
selection = selection > 1 ? selection - 1 : 0
}
}
return nsevent
}
}
.onDisappear {
if keyMonitor != nil {
NSEvent.removeMonitor(keyMonitor!)
keyMonitor = nil
}
}
}
}
Approach I used
Uses keyboard shortcuts on a button
Alternate approach
To use commands (How to detect keyboard events in SwiftUI on macOS?)
Code:
Model
struct Item: Identifiable {
var id: Int
var name: String
}
class Model: ObservableObject {
#Published var items = (0..<100).map { Item(id: $0, name: "Item \($0)")}
}
Content
struct ContentView: View {
#StateObject private var model = Model()
#State private var selectedItemID: Int?
var body: some View {
VStack {
Button("move right") {
moveRight()
}
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
ScrollView(.horizontal) {
LazyHGrid(rows: [GridItem(.fixed(180))]) {
ForEach(model.items) { item in
ItemCell(
item: item,
isSelected: item.id == selectedItemID
)
.onTapGesture {
selectedItemID = item.id
}
}
}
}
}
}
private func moveRight() {
if let selectedItemID {
if selectedItemID + 1 >= model.items.count {
self.selectedItemID = model.items.last?.id
} else {
self.selectedItemID = selectedItemID + 1
}
} else {
selectedItemID = model.items.first?.id
}
}
}
Cell
struct ItemCell: View {
let item: Item
let isSelected: Bool
var body: some View {
ZStack {
Rectangle()
.foregroundColor(isSelected ? .yellow : .blue)
Text(item.name)
}
}
}

SwiftUI - View disappears if animated

I am building a custom segmented control. This is the code that I have written.
struct SegmentedControl: View {
private var items: [String] = ["One", "Two", "Three"]
#Namespace var animation:Namespace.ID
#State var selected: String = "One"
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(items, id: \.self) { item in
Button(action: {
withAnimation(.spring()){
self.selected = item
}
}) {
Text(item)
.font(Font.subheadline.weight(.medium))
.foregroundColor(selected == item ? .white : .accentColor)
.padding(.horizontal, 25)
.padding(.vertical, 10)
.background(zStack(item: item))
}
}
} .padding()
}
}
private func zStack(item: String) -> some View {
ZStack{
if selected == item {
Color.accentColor
.clipShape(Capsule())
.matchedGeometryEffect(id: "Tab", in: animation)
} else {
Color(.gray)
.clipShape(Capsule())
}}
}
}
A control is Blue when it is selected.
It works as expected most of the time like in the following GIF.
However, sometimes if you navigate back and forth very fast, the Color.accentColor moves off screen and disappears as you see in the following GIF. I have used a lot of time but could not fix it.
Sometimes, I get this error.
Multiple inserted views in matched geometry group Pair<String,
ID>(first: "Tab", second: SwiftUI.Namespace.ID(id: 248)) have `isSource:
true`, results are undefined.
Info, It is easier to test it on a physical device rather than a simulator.
Update
This is my all codde including the ContentView and the Modal.
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
NavigationView {
VStack {
Button(action: {
self.isPresented.toggle()
}, label: {
Text("Button")
})
}
}
.sheet(isPresented: $isPresented, content: {
ModalView()
})
}
}
struct ModalView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: TabbarView(),
label: {
Text("Navigate")
})
}
}
}
struct TabbarView: View {
private var items: [String] = ["One", "Two", "Three"]
#Namespace var animation:Namespace.ID
#State var selected: String = "" // change here
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(items, id: \.self) { item in
Button(action: {
withAnimation{
self.selected = item
}
}) {
Text(item)
.font(Font.subheadline.weight(.medium))
.foregroundColor(selected == item ? .white : .accentColor)
.padding(.horizontal, 25)
.padding(.vertical, 10)
.background(zStack(item: item))
}
}
} .padding()
}
.onAppear { self.selected = "One" } // add this
}
private func zStack(item: String) -> some View {
ZStack{
if selected == item {
Color.accentColor
.clipShape(Capsule())
.matchedGeometryEffect(id: "Tab", in: animation)
} else {
Color(.gray)
.clipShape(Capsule())
}}
}
}

SwiftUI - Hide custom onDelete View on tap gesture

I have LazyVStack view that contains a list of views. Each one of the views has a different color and there is 8 points space between them. Threrefore, I can not use List.
So I am trying to build a custom trailing swipe that functions similar to the onDelete method of List. This is my code and it is not perfect, but I am on the right directin, I think.
Test Data - List of countries
class Data: ObservableObject {
#Published var countries: [String]
init() {
self.countries = NSLocale.isoCountryCodes.map { (code:String) -> String in
let id = NSLocale.localeIdentifier(fromComponents: [NSLocale.Key.countryCode.rawValue: code])
return NSLocale(localeIdentifier: "en_US").displayName(forKey: NSLocale.Key.identifier, value: id) ?? "Country not found for code: \(code)"
}
}
}
ContentView
struct ContentView: View {
#ObservedObject var data: Data = Data()
var body: some View {
ScrollView {
LazyVStack {
ForEach(data.countries, id: \.self) { country in
VStack {
SwipeView(content: {
VStack(spacing: 0) {
Spacer()
Text(country)
.frame(minWidth: 0, maxWidth: .infinity)
Spacer()
}
.background(Color.yellow)
}, trailingActionView: {
Image(systemName: "trash")
.foregroundColor(.white)
}) {
self.data.countries.removeAll {$0 == country}
}
}
.clipShape(Rectangle())
}
}
}
.padding(.vertical, 16)
}
}
Custom SwipeView
struct SwipeView<Content: View, TrailingActionView: View>: View {
let width = UIScreen.main.bounds.width - 32
#State private var height: CGFloat = .zero
#State var offset: CGFloat = 0
let content: Content
let trailingActionView: TrailingActionView
var onDelete: () -> ()
init(#ViewBuilder content: () -> Content,
#ViewBuilder trailingActionView: () -> TrailingActionView,
onDelete: #escaping () -> Void) {
self.content = content()
self.trailingActionView = trailingActionView()
self.onDelete = onDelete
}
var body: some View {
ZStack {
HStack(spacing: 0) {
Button(action: {
withAnimation {
self.onDelete()
}
}) {
trailingActionView
}
.frame(minHeight: 0, maxHeight: .infinity)
.frame(width: 60)
Spacer()
}
.background(Color.red)
.frame(width: width)
.offset(x: width + self.offset)
content
.frame(width: width)
.contentShape(Rectangle())
.offset(x: self.offset)
.gesture(DragGesture().onChanged(onChanged).onEnded { value in
onEnded(value: value, width: width)
})
}
.background(Color.white)
}
private func onChanged(value: DragGesture.Value) {
let translation = value.translation.width
if translation < 0 {
self.offset = translation
} else {
}
}
private func onEnded(value: DragGesture.Value,width: CGFloat) {
withAnimation(.easeInOut) {
let translation = -value.translation.width
if translation > width - 16 {
self.onDelete()
self.offset = -(width * 2)
}
else if translation > 50 {
self.offset = -50
}
else {
self.offset = 0
}
}
}
}
It has one annoying problem: If you swipe a row and do not delete it. And if you swipe another views, they don not reset. All the trailing Delete Views are visible. But I want to reset/ swipe back if you tap anywhere outside the Delete View.
I want to swipe back if you tap anywhere outside the Delete View. So how to do it?
First off, to know which cell is swiped the SwipeViews needs an id. If you don't want to set them from external I guess this will do:
struct SwipeView<Content: View, TrailingActionView: View>: View {
...
#State var id = UUID()
...
}
Then you need to track which cell is swiped, the SwiftUI way of relaying data to siblings is by a Binding that is saved in it's parent. Read up on how to pass data around SwiftUI Views. If you want to be lazy you can also just have a static object that saves the selected cell:
class SwipeViewHelper: ObservableObject {
#Published var swipedCell: UUID?
private init() {}
static var shared = SwipeViewHelper()
}
struct SwipeView<Content: View, TrailingActionView: View>: View {
...
#ObservedObject var helper = SwipeViewHelper.shared
...
}
Then you have to update the swipedCell. We want the cell to close when we START swiping on a different cell:
private func onChanged(value: DragGesture.Value) {
...
if helper.swipedCell != nil {
helper.swipedCell = nil
}
...
}
And when a cell is open we save it:
private func onEnded(value: DragGesture.Value,width: CGFloat) {
withAnimation(.easeInOut) {
...
else if translation > 50 {
self.offset = -50
helper.swipedCell = id
}
...
}
}
Then we have to respond to changes of the swipedCell. We can do that by adding an onChange inside the body of SwipeView:
.onChange(of: helper.swipedCell, perform: { newCell in
if newCell != id {
withAnimation(.easeInOut) {
self.offset = 0
}
}
})
Working gist: https://gist.github.com/Amzd/61a957a1c5558487f6cc5d3ce29cf508

SwiftUI Programmatically Select List Item

I have a SwiftUI app with a basic List/Detail structure. A new item is created from
a modal sheet. When I create a new item and save it I want THAT list item to be
selected. As it is, if no item is selected before an add, no item is selected after
an add. If an item is selected before an add, that same item is selected after the
add.
I'll include code for the ContentView, but this is really the simplest example of
List/Detail.
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addNewResort = false
#State private var coverDeletedDetail = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
List {
ForEach(resortStore.resorts) { resort in
NavigationLink(destination: ResortView(resort: resort)) {
HStack(spacing: 20) {
Image("FlatheadLake1")
//bunch of modifiers
VStack(alignment: .leading, spacing: 10) {
//the cell contents
}
}
}
}
.onDelete { indexSet in
self.removeItems(at: [indexSet.first!])
self.coverDeletedDetail.toggle()
}
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}//list
.onAppear(perform: self.selectARow)
.navigationBarTitle("Resorts")
.navigationBarItems(leading:
//buttons
}//body
func removeItems(at offsets: IndexSet) {
resortStore.resorts.remove(atOffsets: offsets)
}
func selectARow() {
//nothing that I have tried works here
print("selectARow")
}
}//struct
And again - the add item modal is extremely basic:
struct AddNewResort: View {
//bunch of properties
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName, country: self.resortCountry, description: self.resortDescription, imageCredit: "Credit", price: Int(self.resortPriceString) ?? 0, size: Int(self.resortSizeString) ?? 0, snowDepth: 20, elevation: 3000, runs: 40, facilities: ["bar", "garage"])
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
To show the issue - The list with a selection:
The list after a new item created showing the previous selection:
Any guidance would be appreciated. Xcode 11.4
I tried to reconstitute your code as closely as could so that it builds. Here is what I have in the end. We have a list of resorts and when a new resort is saved in the AddNewResort sheet, if we are currently in split view (horizontalSizeClass is regular), we will select the new resort, otherwise just dismiss the sheet.
import SwiftUI
class ResortStore: ObservableObject {
#Published var resorts = [Resort(id: UUID(), name: "Resort 1")]
}
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addingNewResort = false
#State var selectedResortId: UUID? = nil
var navigationLink: NavigationLink<EmptyView, ResortView>? {
guard let selectedResortId = selectedResortId,
let selectedResort = resortStore.resorts.first(where: {$0.id == selectedResortId}) else {
return nil
}
return NavigationLink(
destination: ResortView(resort: selectedResort),
tag: selectedResortId,
selection: $selectedResortId
) {
EmptyView()
}
}
var body: some View {
NavigationView {
ZStack {
navigationLink
List {
ForEach(resortStore.resorts, id: \.self.id) { resort in
Button(action: {
self.selectedResortId = resort.id
}) {
Text(resort.name)
}
.listRowBackground(self.selectedResortId == resort.id ? Color.gray : Color(UIColor.systemBackground))
}
}
}
.navigationBarTitle("Resorts")
.navigationBarItems(trailing: Button("Add Resort") {
self.addingNewResort = true
})
.sheet(isPresented: $addingNewResort) {
AddNewResort(selectedResortId: self.$selectedResortId)
.environmentObject(self.resortStore)
}
WelcomeView()
}
}
}
struct ResortView: View {
let resort: Resort
var body: some View {
Text("Resort View for resort name: \(resort.name).")
}
}
struct AddNewResort: View {
//bunch of properties
#Binding var selectedResortId: UUID?
#State var resortName = ""
#Environment(\.presentationMode) var presentationMode
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#EnvironmentObject var resortStore: ResortStore
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName)
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
if self.horizontalSizeClass == .regular {
self.selectedResortId = newResort.id
}
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome View")
}
}
struct Resort {
var id: UUID
var name: String
}
We need to keep track of the selectedResortId
We create an invisible NavigationLink that will programmatically navigate to the selected resort
We make our list row a Button, so that the user can select a resort by tapping on the row
I started writing a series of articles about navigation in SwiftUI List view, there are a lot of points to consider while implementing programmatic navigation.
Here is the one that describes this solution that I'm suggesting: SwiftUI Navigation in List View: Programmatic Navigation. This solution works at the moment on iOS 13.4.1. SwiftUI is changing rapidly, so we have to keep on checking.
And here is my previous article that explains why a more simple solution of adding a NavigationLink to each List row has some problems at the moment SwiftUI Navigation in List View: Exploring Available Options
Let me know if you have questions, I'd be happy to help where I can.

How show list certain row in view?

The list in the view is limited to showing columns. I want to include the column that I want in the columns shown in the view.
How should I do?
#State var showMyRow = false
var body: some view {
Vstack {
Button(“bt”) {
self.showMyRow.toggle()
}
ImageView
....
List {
ForEach
.
.
. // if showMyRow is true, view show this row
}
}
}
Above image, showMyRow is false.
After button action, showMyRow is true.
If showMyRow is true, show me at the 7row
I'm guessing that you're trying to present multiple columns horizontally on the screen?
If so, then you can put the Lists inside a Stack
HStack {
List {
Text("Hello")
}
List {
Text("There")
}
}
And if there's a lot of columns, then you can wrap the HStack inside a scroll view.
var body: some View {
ScrollView(.horizontal) {
HStack {
List {
Text("column 1")
}
List {
Text("column 2")
}
List {
Text("column 3")
}
List {
Text("column 4")
}
}
.frame(width: 1000, height: 800)
.background(Color.red)
}
}
Updated Answer:
Not sure if I understood your question perfectly, but if you just want a list that you can add and delete things from, then the easiest would be to create a separate SwiftUI file holding a struct for the row and an ObservableObject class that checks for any changes.
1) SwiftUI File with ListItem struct & ObservableObject class:
import SwiftUI
struct ListItem: Identifiable {
let id = UUID()
let title: String
}
class Items: ObservableObject {
#Published var rows = [ListItem]()
}
2) ContentView with your image view and list:
This also has a removeItems method so you can swipe any row to delete it.
struct ContentView: View {
#ObservedObject var items = Items()
var body: some View {
NavigationView{
VStack{
Image("image")
.resizable()
.frame(width: 200, height: 200)
.padding(50)
List {
ForEach(items.rows) { item in
Text(item.title)
}
.onDelete(perform: removeItems)
}
}
.navigationBarTitle("My List")
.navigationBarItems(trailing: Button(action : {
let row = ListItem(title: "New Row")
self.items.rows.append(row)
}) {
Image(systemName: "plus")
}
)
}
}
func removeItems(at offsets: IndexSet) {
items.rows.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources