I have a environment object that is an array, and a panel in my settings page where the user can add/remove values to the array...here is the list of items in the array displayed visually for the user:
ScrollView {
VStack {
ForEach(self.env.numbers.indices, id: \.self) { number in
ZStack {
HStack {
Text("#\(number + 1): ")
.font(Font.custom(K.font.montserratMedium, size: 17.0))
.foregroundColor(Color(K.SettingsTextColor))
// Spacer()
NumberTextField(self.env.numbers[number], text: self.$env.numbers[number])
}
}
}
}
}
and at the bottom of my view is a toggle to change the number of items in the array:
Stepper(onIncrement: {
self.env.numbers.append("12345")
print("default number should be added")
print(self.env.numbers.indices)
print(self.env.numbers)
print("count = \(self.env.numbers.count)")
},
onDecrement: {
if self.env.numbers.count != 0 {
print("number should be removed")
self.env.numbers.removeLast(1)
// also tried self.env.numbers = self.env.numbers.droplast() but this didnt work
print(self.env.numbers.indices)
print(self.env.numbers)
print("count = \(self.env.numbers.count)")
}
the NumberTextField view is a UIKit UITextField wrapper, and seems to work fine. Everything about this works fine until I try to decrease the stepper, and I get the Index out of range error (which surprisingly shows the error happening in the app delegate)...any ideas how to fix this?
Related
I have a set of seven pages. I have made my own scrollable view.
I have a working solution where I have the current page and attach gestures to it like this...
switch page {
case .EyeTest:
EyeTestView(sd: $sd)
.gesture(DragGesture(minimumDistance: swipeMin)
.onEnded { value in
if value.translation.width < -swipeMin { page = Page.Tone }
} )
...`
This worked. It was rather clunky, but I could animate the drags better if I worked at it. However, the tab view worked beautifully with TabView for up to five entries. So, I thought I might be able to span the solution using two tab views. Here is what I did, concluding the custom scrollbar at the top...
VStack() {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack() {
pageButton(Page.EyeTest, "eyeglasses", "EyeTest", proxy)
pageButton(Page.Tone, "pause.rectangle", "Tone", proxy)
pageButton(Page.Chart, "square.grid.4x3.fill", "Chart", proxy)
pageButton(Page.ByEye, "eye", "ByEye", proxy)
pageButton(Page.List, "list.bullet", "List", proxy)
pageButton(Page.Camera, "camera", "Camera", proxy)
pageButton(Page.Settings, "gear", "Settings", proxy)
}
}
.onAppear { proxy.scrollTo(Page.ByEye, anchor: .center) }
.onChange(of: page) { page in
withAnimation {
proxy.scrollTo(page, anchor: .center)
}
}
}
if (page == Page.EyeTest || page == Page.Tone || page == Page.Chart) {
TabView(selection:$page) {
EyeTestView(sd: $sd).tag(Page.EyeTest)
ToneView(sd: $sd).tag(Page.Tone)
ChartView(sd: $sd).tag(Page.Chart)
ByEyeView(sd: $sd).tag(Page.ByEye)
ListView(sd: $sd).tag(Page.List)
}.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
} else {
TabView(selection:$page) {
ChartView(sd: $sd).tag(Page.Chart)
ByEyeView(sd: $sd).tag(Page.ByEye)
ListView(sd: $sd).tag(Page.List)
CameraView(sd: $sd).tag(Page.Camera)
SettingsView(sd: $sd).tag(Page.Settings)
}.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
}
}`
However, this does not scroll past the 5th entry.
I would be interested to know why this doesn't work, but I would be happy to replace it with something equivalent that does work. I would also like to know how people handle things like books, which have lost more pages. I feel I ought to make two synchronised scrollable lists.
Yes, maybe it is bad API to have a flat menu with more than five entries. However, some of the ones at the end of the list are rarely used, but need to be discoverable.
I am using Xcode 14.0.1 with iOS 16.0.
Here is a solution that seems to work for me. It is quite close to my original bodge: we have two TabViews, and swap between them depending on the current page so we can always have the neighbours on either side.
I had written my own scrollable index to replace the TabView indexes, but you might make do with the original fittings. I have only been doing Swift for a month, so this may not be the best code, so please post your improvements.
`
enum Page {
case EyeTest
case Tone
case Chart
case ByEye
case List
case Camera
case Settings
}
#AppStorage("menu") private var menu = "Measure"
#State private var page = Page.ByEye
func setMenu(_ page: Page) {
menu = (page == Page.EyeTest || page == Page.Tone || page == Page.Chart ) ? "Test" : "Measure"
}
func pageButton(_ select: Page, _ icon: String, _ title: String, _ proxy: ScrollViewProxy) -> some View {
return Button {
page = select
setMenu(page)
withAnimation {
proxy.scrollTo(select, anchor: .center)
}
} label: {
VStack {
Image(systemName: icon)
Text(title)
}
.frame(width: tabW)
}
.foregroundColor( page == select ? Color.white : Color.gray )
.id(select)
}
var body: some View {
VStack() {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack() {
pageButton(Page.EyeTest, "eyeglasses", "EyeTest", proxy)
pageButton(Page.Tone, "pause.rectangle", "Tone", proxy)
pageButton(Page.Chart, "square.grid.4x3.fill", "Chart", proxy)
pageButton(Page.ByEye, "eye", "ByEye", proxy)
pageButton(Page.List, "list.bullet", "List", proxy)
pageButton(Page.Camera, "camera", "Camera", proxy)
pageButton(Page.Settings, "gear", "Settings", proxy)
}
}
.onAppear { proxy.scrollTo(page, anchor: .center) }
.onChange(of: page) { page in
withAnimation {
proxy.scrollTo(page, anchor: .center)
}
}
}
if (menu == "Test") {
TabView(selection:$page) {
EyeTestView(sd: $sd).tag(Page.EyeTest)
ToneView(sd: $sd).tag(Page.Tone)
ChartView(sd: $sd).tag(Page.Chart)
ByEyeView(sd: $sd).tag(Page.ByEye)
SettingsView(sd: $sd).tag(Page.Settings)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
.onChange(of: page) { page in
setMenu(page)
}
} else {
TabView(selection:$page) {
ChartView(sd: $sd).tag(Page.Chart)
ByEyeView(sd: $sd).tag(Page.ByEye)
ListView(sd: $sd).tag(Page.List)
CameraView(sd: $sd).tag(Page.Camera)
SettingsView(sd: $sd).tag(Page.Settings)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode:.never))
.onChange(of: page) { page in
setMenu(page)
}
}
`
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 following the Apple Developer tutorial on interfacing with UIKIt
The only real difference is that that I'm trying to call
PageView<Page:View> {
from a NavigationLink. The example code within PageView has a default value hardcoded of
#State var currentPage = 0
I'd rather initiate the swipe process on the view that the user selected and not start at the first one and I'm trying to create a simple way to swipe through the items without having to return to the navigationlink to access the next item on the list.
Is there a way to trap the index of the link the user selected and pass this through to PageView as a variable.
I'd tried incrementing a counter and using a function to increment the counter and return the PageView sruct without any success.
animals is an array, just in case that wasn't clear and I'm using the map function to pass in the appropriate value to create the detailed view I'm looking to create . Also, I realize I could probably put the detail view in a scroll view.
Here's my code
var body: some View {
List {
ForEach(collection.animals, id: \.self) { animal in
NavigationLink(destination:PageView(self.collection.animals.map { AnimalDetail(animalMetadata: $0.wrappedMetadata )})) {
AnimalRow(thumbnailImage: Image(uiImage: UIImage(data: animal.image!)!), label: (animal.name!) ).font(.subheadline)
}
}.onDelete(perform: delete)
}
Everything else seems to work fine - just can't figure a way to capture an index value from the list to indicate which item/view was clicked and to pass that value through to PageView in order to create this as the first viewed View.
Thank you !
edit
I tried using .enumerated() on the Array but still couldn't figure out how to get the specific index value to the PAgeView view.
List {
ForEach(Array(collection.animals.enumerated()), id: \.1.id) {index, item in
NavigationLink(destination:PageView( self.collection.animals.map { AnimalDetail(animalMetadata: $0.wrappedMetadata)} )) {
AnimalRow(thumbnailImage: Image(uiImage: UIImage(data: item.image!)!), label: (animal.name!) ).font(.subheadline)
}
}.onDelete(perform: delete)
Try the following...
In PageView:
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage: Int // no initial value
init(_ views: [Page], selection: Int) {
_currentPage = State(initialValue: selection)
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
// ... other code here
}
In your code:
var body: some View {
List {
ForEach(Array(collection.animals.enumerated()), id: \.element) { idx, animal in
NavigationLink(destination:
PageView(self.collection.animals.map {
AnimalDetail(animalMetadata: $0.wrappedMetadata )},
selection: idx)) { // << here !!
AnimalRowRow(thumbnailImage: Image(uiImage: UIImage(data: animal.image!)!),
label: (animal.name!) ).font(.subheadline)
}
}.onDelete(perform: delete)
}
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")) */