SwiftUI: Slider in List/ForEach behaves strangely - ios

It's very hard to explain without a recording from a second device that I don't have, but when I try to slide my slider, it will stop when my finger is definitely still moving.
I have my code posted below. I'd be happy to answer any questions and explain whatever. I'm sure it's something really simple that I should know. Any help would be very much appreciated, thanks!
import SwiftUI
class SettingsViewModel: ObservableObject {
#Published var selectedTips = [
10.0,
15.0,
18.0,
20.0,
25.0
]
func addTip() {
selectedTips.append(0.0)
selectedTips.sort()
}
func removeTip(index: Int) {
selectedTips.remove(at: index)
selectedTips = selectedTips.compactMap{ $0 }
}
}
struct SettingsTipsView: View {
#StateObject var model = SettingsViewModel()
var body: some View {
List {
HStack {
Text("Edit Suggested Tips")
.font(.title2)
.fontWeight(.semibold)
Spacer()
if(model.selectedTips.count < 5) {
Button(action: { model.addTip() }, label: {
Image(systemName: "plus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
ForEach(model.selectedTips, id: \.self) { tip in
let i = model.selectedTips.firstIndex(of: tip)!
//If I don't have this debug line here then the LAST slider in the list tries to force the value to 1 constantly, even if I remove the last one, the new last slider does the same. It's from a separate file but it's pretty much the same as the array above. An explanation would be great.
Text("\(CalculatorViewModel.suggestedTips[i])")
HStack {
Text("\(tip, specifier: "%.0f")%")
Slider(value: $model.selectedTips[i], in: 1...99, label: { Text("Label") })
if(model.selectedTips.count > 1) {
Button(action: { model.removeTip(index: i) }, label: {
Image(systemName: "minus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
}
}

Using id: \.self within a List or ForEach is a dangerous idea in SwiftUI. The system uses it to identify what it expects to be unique elements. But, as soon as you move the slider, you have a change of ending up with a tip value that is equal to another value in the list. Then, SwiftUI gets confused about which element is which.
To fix this, you can use items with truly unique IDs. You should also try to avoid using indexes to refer to certain items in the list. I've used list bindings to avoid that issue.
struct Tip : Identifiable {
var id = UUID()
var tip : Double
}
class SettingsViewModel: ObservableObject {
#Published var selectedTips : [Tip] = [
.init(tip:10.0),
.init(tip:15.0),
.init(tip:18.0),
.init(tip:20.0),
.init(tip:25.0)
]
func addTip() {
selectedTips.append(.init(tip:0.0))
selectedTips = selectedTips.sorted(by: { a, b in
a.tip < b.tip
})
}
func removeTip(id: UUID) {
selectedTips = selectedTips.filter { $0.id != id }
}
}
struct SettingsTipsView: View {
#StateObject var model = SettingsViewModel()
var body: some View {
List {
HStack {
Text("Edit Suggested Tips")
.font(.title2)
.fontWeight(.semibold)
Spacer()
if(model.selectedTips.count < 5) {
Button(action: { model.addTip() }, label: {
Image(systemName: "plus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
ForEach($model.selectedTips, id: \.id) { $tip in
HStack {
Text("\(tip.tip, specifier: "%.0f")%")
.frame(width: 50) //Otherwise, the width changes while moving the slider. You could get fancier and try to use alignment guides for a more robust solution
Slider(value: $tip.tip, in: 1...99, label: { Text("Label") })
if(model.selectedTips.count > 1) {
Button(action: { model.removeTip(id: tip.id) }, label: {
Image(systemName: "minus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
}
}

Related

SwiftUI Picker and Buttons inside same Form section are triggered by the same user click

I have this AddWorkoutView and I am trying to build some forms similar to what Apple did with "Add new contact" sheet form.
Right now I am trying to add a form more complex than a simple TextField (something similar to "add address" from Apple contacts but I am facing the following issues:
in the Exercises section when pressing on a new created entry (exercise), both Picker and delete Button are triggered at the same time and the Picker gets automatically closed as soon as it gets open and the selected entry is also deleted when going back to AddWorkoutView.
Does anyone have any idea on how Apple implemented this kind of complex form like in the screenshow below?
Thanks to RogerTheShrubber response here I managed to somehow implement at least the add button and to dynamically display all the content I previously added, but I don't know to bring together multiple TextFields/Pickers/any other stuff in the same form.
struct AddWorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#EnvironmentObject var dateModel: DateModel
#Environment(\.presentationMode) var presentationMode
#State var workout: Workout = Workout()
#State var exercises: [Exercise] = [Exercise]()
func getBinding(forIndex index: Int) -> Binding<Exercise> {
return Binding<Exercise>(get: { workout.exercises[index] },
set: { workout.exercises[index] = $0 })
}
var body: some View {
NavigationView {
Form {
Section("Workout") {
TextField("Title", text: $workout.title)
TextField("Description", text: $workout.description)
}
Section("Exercises") {
ForEach(0..<workout.exercises.count, id: \.self) { index in
HStack {
Button(action: { workout.exercises.remove(at: index) }) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
Divider()
VStack {
TextField("Title", text: $workout.exercises[index].title)
Divider()
Picker(selection: getBinding(forIndex: index).type, label: Text("Type")) {
ForEach(ExerciseType.allCases, id: \.self) { value in
Text(value.rawValue)
.tag(value)
}
}
}
}
}
Button {
workout.exercises.append(Exercise())
} label: {
HStack(spacing: 0) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
.padding(.trailing)
Text("add exercise")
}
}
}
}
.navigationTitle("Create new Workout")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
}
.accessibilityLabel("Cancel adding Workout")
}
ToolbarItem(placement: .confirmationAction) {
Button {
} label: {
Text("Done")
}
.accessibilityLabel("Confirm adding the new Workout")
}
}
}
}
}

Two extra views put inside of a ForEach with a filtering search bar

So I have a ScrollView that contains a list of all the contacts imported from a user's phone. Above the ScrollView, I have a 'filter search bar' that has a binding that causes the list to show only contacts where the name contains the same string as the search bar filter. For some reason, the last two contacts in the list always pop up at the bottom of the list, no matter what the string is (even if it's a string not contained in any of the contact names on the phone). I tried deleting a contact and the problem persists, because the original contact was just replaced with the new second to last contact. Any help fixing this would be much appreciated!
struct SomeView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: CustomContact = []
var body: some View {
var filteredContactsCount = 0
if friendsFilterText.count != 0 {
for contact in appState.savedContacts {
if contact.name.lowercased().contains(friendsFilterText.lowercased()) {
filteredContactsCount += 1
}
}
} else {
filteredContactsCount = savedContacts.count
}
return HStack {
Image(systemName: "magnifyingglass")
ZStack {
HStack {
Text("Type a name...")
.opacity(friendsFilterText.count > 0 ? 0 : 1)
Spacer()
}
CocoaTextField("", text: $friendsFilterText)
.background(Color.clear)
}
Button(action: {
friendsFilterText = ""
}, label: {
Image(systemName: "multiply.circle.fill")
})
}.frame(height: 38)
HStack(spacing: 10) {
Text("Your contacts (\(filteredContactsCount))")
Spacer()
Button(action: {
fetchContacts()
}, label: {
Image(systemName: "square.and.arrow.down")
})
Button(action: {
// edit button action
}, label: {
Text("Edit")
})
}
ScrollView {
VStack {
ForEach(savedContacts, id: \.self.name) { contact in
if contact.name.lowercased().contains(friendsFilterText.lowercased()) || friendsFilterText.count == 0 {
Button(action: {
// contact button action
}, label: {
HStack(spacing: 20) {
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 41))
.frame(width: 41, height: 41)
VStack(alignment: .leading, spacing: 4) {
Text(contact.name)
Text(contact.phoneNumber)
}
Spacer()
}.frame(height: 67)
})
}
}
}
}
}
}
CustomContact is a custom struct with properties phoneNumber and name. I've attached images below of the issue I'm experiencing. I'm thinking MAYBE it's because there's something off timing-wise with the friendsFilterText and the ForEach rendering but I'm really not sure.
In the image set below, the 'Extra Contact 1' and 'Extra Contact 2' are ALWAYS rendered, unless I add a filter, then switch to a different view, then back to this view (which leads me to believe it's a timing thing again).
https://imgur.com/a/CJW2CUS
You should move the count calculation out of the view into a computed var.
And if CustomContact is your single contact struct, it should actually read #State var savedContacts: [CustomContact] = [] i.e. an array of CustomContact.
The rest worked fine with me, no extra contacts showing.
struct ContentView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: [CustomContact] = []
// computed var
var filteredContactsCount: Int {
if friendsFilterText.isEmpty { return savedContacts.count }
return savedContacts.filter({ $0.name.lowercased().contains(friendsFilterText.lowercased()) }).count
}
var body: some View {
...

How to add a divider between each item inside a ForEach View?

I'm trying to build a ForEach that is looping through an array of objects. Everything is working fine, but I cannot figure out how to add a Divider between the elements.
The layout for the rows is in a separate view, and I have tried adding a Divider to the row, which is causing the end of the list to look pretty bad, because of the Divider below the last item.
I cannot use a List, because it is not the only view on the page. Everything is inside a ScrollView.
For reference, here is the code as well as the UI so far.
This is the code of the List view:
VStack {
ForEach (manufacturers) { manufacturer in
NavigationLink(destination: Text("test")) {
Row(manufacturer: manufacturer)
}
}
}
.background(Color("ListBackground"))
.cornerRadius(12)
This is the code of the Row view:
VStack {
HStack {
Text(manufacturer.name)
.font(.system(size: 18, weight: .regular))
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
}
.padding()
.buttonStyle(PlainButtonStyle())
Is there a way to add a Divider between every item in the ForEach loop, or am I able to remove the Divider from the last item?
I'm happy about every help I can get.
Here is a possible approach
ForEach (manufacturers) { manufacturer in
VStack {
NavigationLink(destination: Text("test")) {
Row(manufacturer: manufacturer)
}
// I don't known your manufacturer type so below condition might
// require fix during when you adapt it, but idea should be clear
if manufacturer.id != manufacturers.last?.id {
Divider()
}
}
}
You can remove last line with compare count.
struct Row: View {
var manufacturer: String
var isLast: Bool = false
var body: some View {
VStack {
HStack {
Text(manufacturer)
.font(.system(size: 18, weight: .regular))
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
if !isLast {
Divider()
}
}
.padding()
.buttonStyle(PlainButtonStyle())
}
}
struct ContentView5: View {
private var manufacturers = ["1", "2", "3"]
var body: some View {
VStack {
ForEach (manufacturers.indices, id: \.self) { idx in
NavigationLink(destination: Text("test")) {
Row(manufacturer: manufacturers[idx], isLast: idx == manufacturers.count - 1)
}
}
}
.background(Color("ListBackground"))
.cornerRadius(12)
}
}
Or you can remove last var from Row.
VStack {
ForEach (manufacturers.indices, id: \.self) { idx in
NavigationLink(destination: Text("test")) {
Row(manufacturer: manufacturers[idx])
}
if idx != manufacturers.count - 1 {
Divider()
}
}
}
You could use a generic "DividedForEach" View which inserts a divider after every element except the last.
struct DividedForEach<Data: RandomAccessCollection, ID: Hashable, Content: View, D: View>: View {
let data: Data
let id: KeyPath<Data.Element, ID>
let content: (Data.Element) -> Content
let divider: (() -> D)
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (Data.Element) -> Content, divider: #escaping () -> D) {
self.data = data
self.id = id
self.content = content
self.divider = divider
}
var body: some View {
ForEach(data, id: id) { element in
content(element)
if element[keyPath: id] != data.last![keyPath: id] {
divider()
}
}
}
}

Swift 2.0 .contextMenu Multiple Delete From Core Data

First time post in here and new to coding... so I hope I am following proper protocol. I am putting together a view in Xcode 12.2 (SwiftUI 2) that outputs a list of data from Core Data and have a context menu to provide the user the option to edit, delete, and delete multiple. The context menu is working properly for edit and delete, however, I am facing a road block in how to implement the functionality to delete multiple list items. I am imagining the user would hard press one of the list items, the context menus pops open and if they press the "Delete Multiple" option, the view activates something similar to an edit mode that populates little circle on the left of each item which the user can select and delete more than one item at a time. I can see other article on how to do this, however, I cannot find guidance on how to implement this through Core Data. I have pasted my code below.
Please let me know if I am missing any other information that would make my question more clear.
I really appreciate the forums expertise and guidance.
Struct List : View {
#StateObject var appData = AppViewModel()
#Environment(\.managedObjectContext) var context
//Fetch Data...
#FetchRequest(entity: EntryData.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], animation: .spring()) var results : FetchedResults<EntryData>
var body : some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom), content: {
VStack{
ScrollView(.vertical, showsIndicators: false, content: {
LazyVStack(alignment: .leading, spacing: 20){
ForEach(results){task in
VStack(alignment: .leading, spacing: 5, content: {
Text(task.category ?? "")
.font(.title)
.fontWeight(.bold)
Text(task.date ?? Date(), style:. date)
.fontWeight(.bold)
Text("\(task.number.formattedCurrencyText)")
})
.padding(.horizontal, 14)
.padding(.top, 10)
.foregroundColor(Color("ColorTextList"))
.contextMenu{
Button(action: {appData.editItem(item: task)}, label: {
Text("Edit")
})
Button(action: {
context.delete(task)
try! context.save()
}, label: {
Text("Delete")
})
Button(action: {}, label: {
Text("Delete Mutiple")
})
}
}
}
})
}
VStack(){
VisualEffectView(effect: UIBlurEffect(style: .regular))
.frame(width: UIScreen.main.bounds.width, height: 50, alignment: .top)
.edgesIgnoringSafeArea(.all)
.background(Color.clear)
Spacer()
}
})
.background(Color.clear)
.sheet(isPresented: $appData.isNewData, content: {
AddDataView(appData: appData)
.environment(\.managedObjectContext, self.context)
})
}
}
Adding the viewModel of the app. How do I tap into into this and delete each of the attributes in a multi-list selection?
import SwiftUI
import CoreData
class AppViewModel : ObservableObject, Identifiable{
#Published var cateogry = ""
#Published var date = Date()
#Published var number : Double? = nil
#Published var notes = ""
#Published var id = UUID()
}
And adding the actual Core Data model screenshot.
You can implement a Selection Set for your List. This will contain all elements that are selected. Then you can dynamically show the contextMenu for delete or deleteAll based on the count of the set. Here is a full example with the implementation of deleteAll
struct SelectionDemo : View {
#State var demoData = ["Dave", "Tom", "Phillip", "Steve"]
#State var selected = Set<String>()
var body: some View {
HStack {
List(demoData, id: \.self, selection: $selected){ name in
Text(name)
.contextMenu {
Button(action: {
//Delete only one item
}){
Text("Delete")
}
if (selected.count > 1) {
Button(action: {
//Delete all
deleteAll()
})
{
Text("Delete all")
}
}
}
}.frame(width: 500, height: 460)
}
}
func deleteAll() {
for element in selected {
self.demoData.removeAll(where: {
$0 == element
})
}
}
}

The compiler is unable to type-check this expression in a reasonable time in SwiftUI?

I have a line of code that sets the background of Text to an Image that is fetched by finding the first three letters of the string. For some reason this won't run and keeps giving me the error above. Any ideas on how I can fix this?
There are a lot of images that need to be set as the backgrounds for multiple different pieces of text. I believe I have the right idea by using the prefix of the string, but it seems like Xcode is having difficulty/won't run this.
Pretty sure this specific line is giving me issues, but would love some feedback.
.background(Image(colorOption.prefix(3)).resizable())
import SwiftUI
struct ColorView: View {
// #ObservedObject var survey = Survey()
#ObservedObject var api = ColorAPIRequest(survey: DataStore.instance.currentSurvey!)
#State var showingConfirmation = true
#State var showingColorView = false
#State var tempSelection = ""
#EnvironmentObject var survey: Survey
//#EnvironmentObject var api: APIRequest
var colorOptionsGrid: [[String]] {
var result: [[String]] = [[]]
let optionsPerRow = 4
api.colorOptions.dropFirst().forEach { colorOption in
if result.last!.count == optionsPerRow { result.append([]) }
result[result.count - 1].append(colorOption)
}
return result
}
var body: some View {
VStack {
Text("Select Tape Color")
.font(.system(size:70))
.bold()
.padding(.top, 20)
NavigationLink("", destination: LengthView(), isActive: $showingColorView)
HStack {
List {
ForEach(colorOptionsGrid, id: \.self) { colorOptionRow in
HStack {
ForEach(colorOptionRow, id: \.self) { colorOption in
Button(action: {
// self.survey.length = lengthOption
self.tempSelection = colorOption
self.showingConfirmation = false
}
) {
ZStack {
Color.clear
Text(colorOption.prefix(3))
.font(.title)
.foregroundColor(self.tempSelection == colorOption ? Color.white : Color.black)
.frame(width: 200, height: 100)
.background(Image(colorOption.prefix(3)).resizable())
//Image(colorOption.prefix(3)).resizable()
}
}.listRowBackground(self.tempSelection == colorOption ? Color.pink : Color.white)
.multilineTextAlignment(.center)
}
}
}
}.buttonStyle(PlainButtonStyle())
}
Button(action: {
self.survey.color = self.tempSelection
self.showingColorView = true
self.showingConfirmation = true
}) {
Text("Press to confirm \(tempSelection)")
.bold()
.padding(50)
.background(Color.pink)
.foregroundColor(.white)
.font(.system(size:40))
.cornerRadius(90)
}.isHidden(showingConfirmation)
.padding(.bottom, 50)
}
}
}
The compiler actually gives a fairly decent suggestion when it tells you to break the expression up. The simplest you can do is extract the background image into a separate function like this:
func backgroundImage(for colorOption: String) -> some View {
Image(String(colorOption.prefix(3))).resizable()
}
and then replace the call to
.background(Image(colorOption.prefix(3)).resizable())
with
.background(self.backgroundImage(for: colorOption))
Also note that I wrapped colorOption.prefix(3) in a String constructor, simply because .prefix(_:) returns a Substring, but the Image(_:) constructor requires a String.

Resources