I'm trying to present a list, of witch the first item is a NavigationLink, because its configuration needs some extra options to be defined. All other operations can be run w/o special parameters.
(Code stripped down a bit):
let operations = ["start", "stop", "set_marker", "save_map", "get_status"]
NavigationView {
VStack {
Form {
Section(header: Text("Header")) {
List(operations, id: \.self) { operation in
if operation == "start" {
NavigationLink(destination: DetailView(operation: operation)) {
Text(operation.uppercased())
}
} else {
Button(action: {
switch operation {
case "stop":
break
default:
break
}
}) {
Text(operation.uppercased()).foregroundColor(.black)
}
}
}
}
}
}
}
The thing compiles fine and displays fine too. At runtime I can click all "plain buttons" and it works. For the "start" entry I can open the second window just once. After return the "start" button does not react anymore. Any ideas?
Disregard. Seems to be an issue on emulator and in preview only.
Related
I am attempting to trigger an action within a dynamic island by pressing a button, but the button is not functioning and no action is being executed.
dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
DynamicIslandExpandedRegion(.leading) {
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom")
** Button {
//Print here
print("Pressed")
} label: {
Text("Press Me")**
}.padding()
}
} compactLeading: {
I have tried testing in a new project, but I am still encountering the same problem with the button not firing any action.
I'm facing a weird issue with contextMenu(forSelectionType:menu:primaryAction:) attached to a List. It works fine if you enable edit mode, and start selecting the rows by tapping, but if you have a button that what it does is manually modify the selection, the returned rows when the contextMenu is invoked is incorrect.
Furthermore, if you use the select all button, but actually scroll to the bottom of the list, the returned values is correct, so it seems that unless the cell is rendered, the contextMenu won't return it.
Does anybody know if I'm doing something wrong? Here's a quick example to reproduce the issue:
struct ContentView: View {
let rows = (0..<100).map{ "Row: \($0)" }
#State var selection: Set<String> = []
var body: some View {
List(selection: $selection) {
ForEach(rows, id: \.self) { row in
Text(row).tag(row)
}
}.contextMenu(forSelectionType: String.self) { contextMenuRows in
Button("Number of rows in the contextMenu: \(contextMenuRows.count)") {}
}.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
if selection.isEmpty {
Button("Select All") { selection = Set(rows) }
} else {
Button("Deselect All") { selection = [] }
}
}
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
}
Make sure to embed the ContentView inside a NavigationView to be able to see the navigation bar.
Video demo showing the issue: https://imgur.com/a/fxKk5Cs
Works fine when selecting manually
When selecting all only displays the first 9 rows
After scrolling, all rows are available to the contextMenu
There is a difference between contextMenuRows (the menu parameter) and selection. This can be verified with:
Button("Number of rows in the contextMenu: \(contextMenuRows.count) \(selection.count)")
The use of selection on menu closure is perhaps what you are looking for.
(It's weird that the "Edit/Select All" menu on the Mac, sets contextMenuRows equal to selection)
The issue can be reproduced consistently with the code below. Xcode 13.3 + iOS 15.4 (both are latest).
enum ListID: String, CaseIterable, Hashable, Identifiable {
case list1 = "List1"
case list2 = "List2"
var id: Self {
self
}
}
struct ContentView: View {
#State var listID: ListID = .list1
var body: some View {
VStack {
// 1) Picker
Picker(selection: $listID) {
ForEach(ListID.allCases) { id in
Text(id.rawValue)
}
} label: {
Text("Select a list")
}
.pickerStyle(.segmented)
// 2) List
switch listID {
case .list1:
createList(Array(1...2), id: .list1)
case .list2:
createList(Array(101...102), id: .list2)
}
}
}
#ViewBuilder func createList(_ itemValues: [Int], id: ListID) -> some View {
List {
ForEach(itemValues, id:\.self) { value in
Text("\(value)")
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button("Edit") {
// do nothing
}
.tint(.blue)
}
}
}
.id(id)
}
}
Steps to reproduce the issue:
start up the app. See picture 1.
swipe item 1 in list 1, keep the "Edit" button untouched (don't click on it). See picture 2
then select list 2 in the picker. You should see there is an extra space before the items in the list. What's more, all list items are not swipable any more. See picture 3.
then select list 1 in the picker. It has the same issue. See picture 4.
The issue is not specific to picker. For example, it can be reproduced if we replace picker with a set of buttons. I believe the issue occurs only if the old list is destroyed in SwiftUI view hierarchy. From my understanding of structured identity in SwiftUI, list 1 and list 2 are considered separate views in SwiftUI view hiearch. So it's not clear how they could affect each other. The only reason I can guess is that, while list 1 and list 2 are considered separate virtual views, SwiftUI actually use the same physical view for them (e.g., for performance purpose, etc.). So it seems a SwiftUI bug to me.
Thinking along that line, I can work round the issue by not destroying lists:
ZStack {
createList(Array(1...2), id: .list1)
.opacity(listID == .list1 ? 1 : 0)
createList(Array(101...102), id: .list2)
.opacity(listID == .list2 ? 1 : 0)
}
This works perfectly in this specific example, but unfortunately it's not scalable. For example, in my calendar app when user clicks on a date in the calendar, I'd like to show a list of events on the date (I'd like to use different list for different date. I do so by calling id() to set different id to each list). There isn't an obvious/elegant way to apply the above work around in this case.
So I wonder if anyone knows how to work around the issue in a more general way? Thanks.
I end up working around the issue by using a single virtual view for different lists. To that end, I need to move List outside switch statement (otherwise SwiftUI's structured identity mechanism would consider the two lists as different ones).
The workaround works reliably in my testing (including testing in my actual app). It's clean and general. I prefer to assigning a different id to each list because I think it's clean and better in architecture, but unfortunately it's not usable until Apple fixes the bug. I have submitted FB9976079 about this issue.
I'll keep my question open and welcome anyone leave your answer or comments.
enum ListID: String, CaseIterable, Hashable, Identifiable {
case list1 = "List1"
case list2 = "List2"
var id: Self {
self
}
}
struct ContentView: View {
#State var listID: ListID = .list1
var body: some View {
VStack {
// 1) Picker
Picker(selection: $listID) {
ForEach(ListID.allCases) { id in
Text(id.rawValue)
}
} label: {
Text("Select a list")
}
.pickerStyle(.segmented)
// 2) List
List {
switch listID {
case .list1:
createSection(Array(1...2), id: .list1)
case .list2:
createSection(Array(101...105), id: .list2)
}
}
}
}
// Note: the id param is not used as List id.
#ViewBuilder func createSection(_ itemValues: [Int], id: ListID) -> some View {
Section {
ForEach(itemValues, id:\.self) { value in
Text("\(value)")
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button("Edit") {
// do nothing
}
.tint(.blue)
}
}
}
.id(id)
}
}
I'm writing a fairly simple SwiftUI app about movies and I have this issue where the new .searchable modifier on NavigationView is always being shown, whereas it should be hidden, unless you pull down on the List.
It hides it correctly if I scroll a bit up, and if I scroll down it also hides it correctly, but other than that, it's always being shown. See gif for clarification. (basically it should behave the same as in Messages app)
https://imgur.com/R2rsqzh
My code for using the searchable is fairly simple:
var body: some View {
NavigationView {
List {
ForEach(/*** movie stuff ***/) { movie in
///// row here
}
}
.listStyle(GroupedListStyle())
.onAppear {
// load movies
}
.navigationTitle("Discover")
.searchable(text: $moviesRepository.searchText, placement: .toolbar, prompt: "Search...")
}
}
}
So, after adding a progress view above the list view, it suddenly started working the way I want it to. The code now is minimally changed and looks like this.
var body: some View {
NavigationView {
VStack {
if /** we're doing search, I'm checking search text **/ {
ProgressView()
.padding()
}
if !movies.isEmpty {
List {
ForEach(/** movies list **/) { movie in
// movie row here
}
}
.listStyle(GroupedListStyle())
.navigationTitle("Discover")
.searchable(text: $moviesRepository.searchText, placement: .toolbar,
prompt: Text("Search...")
.foregroundColor(.secondary)
)
} else {
ProgressView()
.navigationTitle("Discover")
}
}
}
.onAppear {
// load movies
}
}
And basically after adding the progress views, it started working the way I described it in my OP and the way it worked for ChrisR
I am learning Swift and trying to implement the "Room" app demonstrated in the WWDC 2019 Session 204 . In my code below, which is exactly typed same as Jacob in the video but I run into the following error:
Line:
.onDelete(perform: deleteRoom)
Error:
"Extraneous argument label 'perform:' in call"
Can't figure out on my own...
Thanks in advance!
struct ContentView: View {
//var rooms: [Room] = []
// #ObservedObject var store = RoomStore()
var store = RoomStore()
var body: some View {
NavigationView {
List {
Section {
Button(action: addRoom) {
Text("Add")
}
}
Section {
ForEach(store.rooms) { room in
RoomCell(room: room)
}
/* HERE is the error */
.onDelete(perform: deleteRoom)
}
}
.navigationBarTitle(Text("Rooms"))
.listStyle(.grouped)
}
}
func addRoom() {
store.rooms.append(Room(name: "New Room", capacity: 20, hasVideo: true))
}
func deleteRoom(at offsets: IndexSet) {
store.rooms.remove(atOffsets: offsets)
}
}
Don't Trust Xcode:
Xcode is not very intelligent to tell you what is the real issue in SwiftUI enough (yet). So believing or not, the issue is with the listStyle.
You should change it to:
.listStyle(GroupedListStyle())
Don't forget to remove the . from .Text("Add") you accidentally typed in first section.
Some other useful notes (NOT related to the issue):
SwiftUI API is now more compatible with Strings, So you can set the Text value directly in some initializers for Views like Button and modifiers like navigationBarTitle:
Button("Add", action: addRoom) /* Instead of Button(action: addRoom) { Text("Add") } */
.navigationBarTitle("Rooms") /* Instead of .navigationBarTitle(Text("Rooms")) */