I have a local JSON file I am importing and decoding. I am them iterating through that data to create a list. I have a Button and I want to toggle the value of the favorite property when the button is tapped. I realize that would be mutating a JSON value which wouldnt work so I am trying to figure out how to accomplish this.
Towns.json
[
{
"display_name": "California",
"favorite": false,
},
{
"display_name": "Colorado",
"favorite": false,
}
]
Town.swift
struct Town: Codable, Identifiable {
var id: String {image}
let display_name: String
let favorite: Bool
}
MainView.swift
ForEach(towns) { town in
LazyVStack(spacing: 20) {
HStack {
Text(town.display_name)
Spacer()
Button {
town.favorite.toggle()
} label: {
if town.favorite {
Image(systemName: "flame").foregroundColor(.red)
} else {
Image(systemName: "flame.fill").foregroundColor(.red)
}
}
}
}
}
You'll need a way to access the original element from the array. In SwiftUI 3 (just announced), this has become much easier, but until that's out, generally people use indices or enumerated to get an index of the original item (there's also a .indexed() from Swift Collections, but it requires importing an SPM package to use it):
struct ContentView : View {
#State var towns : [Town] = []
var body: some View {
ForEach(Array(towns.enumerated()), id: \.1.id) { (index,town) in
LazyVStack(spacing: 20) {
HStack {
Text(town.display_name)
Spacer()
Button {
towns[index].favorite.toggle()
} label: {
if town.favorite {
Image(systemName: "flame").foregroundColor(.red)
} else {
Image(systemName: "flame.fill").foregroundColor(.red)
}
}
}
}
}
}
}
You'll also need to change let favorite to var favorite in your model, since now it's a mutable property.
Related
I'm displaying sort options in my Menu, but I'd also like the user to control the sort direction in the way the Files app works: tapping a second time toggles the sort direction.
Here's what I have:
#State private var selectedSort: SortOption = .name
#State private var isSortAscending = true
enum SortOption {
case name
case number
case length
}
Menu {
Picker(selection: $selectedSort, label: Text("Sorting options")) {
Button {
isSortAscending.toggle()
} label: {
HStack {
Text("Name")
Spacer()
Image(systemName: isSortAscending ? "chevron.down" : "chevron.up")
}
}
.tag(SortOption.name)
Button {
isSortAscending.toggle()
} label: {
HStack {
Text("Number")
Spacer()
Image(systemName: isSortAscending ? "chevron.down" : "chevron.up")
}
}
.tag(SortOption.number)
Button {
isSortAscending.toggle()
} label: {
HStack {
Text("Length")
Spacer()
Image(systemName: isSortAscending ? "chevron.down" : "chevron.up")
}
}
.tag(SortOption.length)
}
}
Tapping doesn't toggle the sort state at all. Is there a better or more supported way to do this?
Item selected is handled internally, so we need selection side-effect. It is possible to do by injecting will-set side effect in computable binding.
Here is a possible approach tested with Xcode 13.4 / iOS 15.5
Main part:
var sorting: Binding<SortOption> { .init(
get: { self.selectedSort },
set: {
if self.selectedSort == $0 {
self.isSortAscending.toggle()
}
self.selectedSort = $0
}
)}
var body: some View {
Menu("Sort") {
Picker(selection: sorting, label: Text("Sorting options")) {
ForEach(SortOption.allCases) { option in
HStack {
Text(option.rawValue)
Spacer()
if selectedSort == option {
Image(systemName: isSortAscending ? "chevron.down" : "chevron.up")
}
Test module on GitHub
import SwiftUI
struct MemoListView: View {
let folder : FolderModel
#State private var showActionSheet : Bool = false
#EnvironmentObject var vm : FolderListViewModel
var body: some View {
ZStack {
if folder.memo.count == 0 {
NoMemoView()
} else {
List {
ForEach(folder.memo) { memo in
MemoRowView(memo: memo, folder: self.folder)
.onLongPressGesture {
self.showActionSheet.toggle()
}
.confirmationDialog(Text("Option"), isPresented: $showActionSheet) {
Button(role : .destructive, action: {
vm.deleteMemo(folder: folder, memo: memo)
}, label: {
Text("Delete")
.foregroundColor(.red)
})
}
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
}
.navigationTitle("Memos in '\(folder.folderName)'")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
NavigationLink(destination: {
NewMemoView(folder: self.folder)
}, label: {
Image(systemName: "plus")
})
NavigationLink(destination: {
}, label: {
Image(systemName: "gear")
})
}
}
}
}
}
Hi!
Please check my code above.
I want to apply actionSheet(confirmationDialog) to each of row in list but I think the list can't recognize which row is selected.
If I tried to delete row3, it just delete only row1
I don't know how I can handle this situation.
Thanks!
As per your comments to the original post, .confirmationDialog only has an isPresented binding available, rather than an object-based one as with sheet, fullScreenCover, etc.
One workaround I've used quite successfully is to move the confirmationDialog, and the boolean state flag that drives it, into a child view. This could be MemoRowView itself, or you could keep that view as the presentation component only and wrap it in an interactive view that called MemoRowView and added the interactive elements, e.g.
// MemoListView
List {
ForEach(folder.memo) { memo in
MemoListItem(memo: memo, folder: self.folder)
}
.listStyle(.plain)
This does mean injecting quite a lot of domain knowledge into the list item, so it'd need its own #EnvironmentObject reference, etc., but that could be lived with.
Alternatively, you could keep the code that deletes the object in the parent view, and just keep the confirmation in the child view:
struct MemoListItem: View {
var memo: MemoModel
var folder: FolderModel
var onDelete: () -> Void
#State private var showDeleteConfirmation: Bool = false
init(memo: MemoModel, folder: FolderModel, onDelete: #escaping () -> Void) {
self.memo = memo
self.folder = folder
self.onDelete = onDelete
}
var body: some View {
// rest of row setup omitted for brevity
.confirmationDialog(Text("Option"), isPresented: $showDeleteConfirmation) {
Button("Delete", role: .destructive, action: onDelete)
}
}
}
// MemoListView
List {
ForEach(folder.memo) { memo in
MemoListItem(memo: memo, folder: self.folder, onDelete: {
vm.deleteMemo(folder: folder, memo: memo)
})
}
}
An explanation of what I wrote in my comment :
struct MemoListView: View {
#EnvironmentObject var vm : FolderListViewModel
// Updated folder so as it seems it is extracted from your
// view model
var folder : FolderModel {
vm.folder
}
#State private var showActionSheet : Bool = false
// This is the way to know which memo to delete
#State var memoToDelete: Memo?
var body: some View {
ZStack {
if folder.memo.count == 0 {
NoMemoView()
} else {
List {
ForEach(folder.memo) { memo in
MemoRowView(memo: memo, folder: self.folder)
.onLongPressGesture {
// Save the memo on the current row
memoToDelete = memo
self.showActionSheet.toggle()
}
.confirmationDialog(Text("Option"), isPresented: $showActionSheet) {
Button(role : .destructive, action: {
// Delete the saved memo
if let memo = memoToDelete {
vm.deleteMemo(folder: folder, memo: memo)
}
}, label: {
// Here to show the memo id before delete
if let memo = memoToDelete {
Text("Delete \(memo.id)")
.foregroundColor(.red)
}
})
}
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
}
.navigationTitle("Memos in '\(folder.folderName)'")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
NavigationLink(destination: {
NewMemoView(folder: self.folder)
}, label: {
Image(systemName: "plus")
})
NavigationLink(destination: {
}, label: {
Image(systemName: "gear")
})
}
}
}
}
}
Note : I changed from the comment and kept the dialog on each row.
It is not the best way to do it.
In a form, I'd like a user to be able to dynamically maintain a list of phone numbers, including adding/removing numbers as they wish.
I'm currently maintaining the list of numbers in a published array property of an ObservableObject class, such that when a new number is added to the array, the SwiftUI form will rebuild the list through its ForEach loop. (Each phone number is represented as a PhoneDetails struct, with properties for the number itself and the type of phone [work, cell, etc].)
Adding/removing works perfectly fine, but when I attempt to edit a phone number within a TextField, as soon as I type a character, the TextField loses focus.
My instinct is that, since the TextField is bound to the phoneNumber property of one of the array items, as soon as I modify it, the entire array within the class publishes the fact that it's been changed, hence SwiftUI dutifully rebuilds the ForEach loop, thus losing focus. This behavior is not ideal when trying to enter a new phone number!
I've also tried looping over an array of the PhoneDetails objects directly, without using an ObservedObject class as an in-between repository, and the same behavior persists.
Below is the minimum reproducible example code; as mentioned, adding/removing items works great, but attempting to type into any TextField immediately loses focus.
Can someone please help point me in the right direction as to what I'm doing wrong?
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
func deletePhoneNumber(at index: Int) {
if allPhones.indices.contains(index) {
allPhones.remove(at: index)
}
}
}
struct PhoneDetails: Equatable, Hashable {
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach(userPhonesManager.allPhones, id: \.self) { phoneDetails in
let index = userPhonesManager.allPhones.firstIndex(of: phoneDetails)!
HStack {
Button(action: { userPhonesManager.deletePhoneNumber(at: index) }) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
try this:
ForEach(userPhonesManager.allPhones.indices, id: \.self) { index in
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(at: index)
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
EDIT-1:
Reviewing my comment and in light of renewed interest, here is a version without using indices.
It uses the ForEach with binding feature of SwiftUI 3 for ios 15+:
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
// -- here --
func deletePhoneNumber(of phone: PhoneDetails) {
allPhones.removeAll(where: { $0.id == phone.id })
}
}
struct PhoneDetails: Identifiable, Equatable, Hashable {
let id = UUID() // <--- here
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach($userPhonesManager.allPhones) { $phone in // <--- here
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(of: phone) // <--- here
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $phone.phoneNumber) // <--- here
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
I want to change another unrelated #State variable when a Picker gets changed, but there is no onChanged and it's not possible to put a didSet on the pickers #State. Is there another way to solve this?
Deployment target of iOS 14 or newer
Apple has provided a built in onChange extension to View, which can be used like this:
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor, label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
}
}
Deployment target of iOS 13 or older
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
}
func colorChange(_ tag: Int) {
print("Color tag: \(tag)")
}
}
Using this helper
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
return Binding(
get: { self.wrappedValue },
set: { selection in
self.wrappedValue = selection
handler(selection)
})
}
}
First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.
Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)
struct SwiftUIView: View {
#State private var selection = 0
var body: some View {
Picker(selection: $selection, label: Text("Some Label")) {
ForEach(0 ..< 5) {
Text("Number \($0)") }
}.onChange(of: selection) { _ in
print(selection)
}
}
}
The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"
I think this is simpler solution:
#State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]
// USE this if needed to notify parent
#Binding var notifyParentOnChangeIndex: Int
var body: some View {
let pi = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// USE this if needed to notify parent
self.notifyParentOnChangeIndex = $0
})
return VStack{
Picker(selection: pi, label: Text("Yolo")) {
ForEach(self.yourData.indices) {
Text(self.yourData[$0])
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}
I know this is a year old post, but I thought this solution might help others that stop by for a visit in need of a solution. Hope it helps someone else.
import Foundation
import SwiftUI
struct MeasurementUnitView: View {
#State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
var userSettings: UserSettings
var body: some View {
VStack {
Spacer(minLength: 15)
Form {
Section {
Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
ForEach(0..<unitTypes.count, id: \.self) {
Text(unitTypes[$0])
}
}.onReceive([self.selectedIndex].publisher.first()) { (value) in
self.savePick()
}
.navigationBarTitle("Change Unit Type", displayMode: .inline)
}
}
}
}
func savePick() {
if (userSettings.unit != unitTypes[selectedIndex]) {
userSettings.unit = unitTypes[selectedIndex]
}
}
}
I use a segmented picker and had a similar requirement. After trying a few things I just used an object that had both an ObservableObjectPublisher and a PassthroughSubject publisher as the selection. That let me satisfy SwiftUI and with an onReceive() I could do other stuff as well.
// Selector for the base and radix
Picker("Radix", selection: $base.value) {
Text("Dec").tag(10)
Text("Hex").tag(16)
Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })
base has both an objectWillChange and a PassthroughSubject<Int, Never> publisher imaginatively called publisher.
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
typealias ObservableInt = Observable<Int>
Defining objectWillChange isn't strictly necessary but when I wrote that I liked to remind myself that it was there.
For people that have to support both iOS 13 and 14, I added an extension which works for both. Don't forget to import Combine.
Extension View {
#ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: #escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: completion)
} else {
self.onReceive([value].publisher.first()) { (value) in
completion(value)
}
}
}
}
Usage:
Picker(selection: $selectedIndex, label: Text("Color")) {
Text("Red").tag(0)
Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
print("Do something with \(newIndex)")
}
Important note: If you are changing a published property inside an observed object within your completion block, this solution will cause an infinite loop in iOS 13. However, it is easily fixed by adding a check, something like this:
.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
if shouldShowSheet {
self.router.currentSheet = .chosenSheet
showSheet = false
}
})
SwiftUI 1 & 2
Use onReceive and Just:
import Combine
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
Picker("Some Label", selection: $selection) {
ForEach(0 ..< 5, id: \.self) {
Text("Number \($0)")
}
}
.onReceive(Just(selection)) {
print("Selected: \($0)")
}
}
}
iOS 14 and CoreData entities with relationships
I ran into this issue while trying to bind to a CoreData entity and found that the following works:
Picker("Level", selection: $contact.level) {
ForEach(levels) { (level: Level?) in
HStack {
Circle().fill(Color.green)
.frame(width: 8, height: 8)
Text("\(level?.name ?? "Unassigned")")
}
.tag(level)
}
}
.onChange(of: contact.level) { _ in savecontact() }
Where "contact" is an entity with a relationship to "level".
The Contact class is an #ObservedObject var contact: Contact
saveContact is a do-catch function to try viewContext.save()...
The very important issue : we must pass something to "tag" modifier of Picker item view (inside ForEach) to let it "identify" items and trigger selection change event. And the value we passed will return to Binding variable with "selection" of Picker.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})
A simple way to express this would be to go back to UITableView and have a didSelectRowAt(indexPath) function that behaved like this:
if (indexPath.row == 0) { ... } else { ... }
Where based upon the indexPath.row value, I can call a unique view controller (ex: the first one is a TableView and the others are CollectionViews.
Currently, based upon the two answers thus far, I can produce the following code:
import SwiftUI
struct MenuItem {
let title: String
let isEnabled: Bool
}
struct HomeList: View {
let menuItems = [
MenuItem(title: "ABC", isEnabled: true),
MenuItem(title: "DEF", isEnabled: false),
MenuItem(title: "GHI", isEnabled: true)
]
var body: some View {
NavigationView {
List {
ForEach(menuItems.indices, id: \.self) { index in
NavigationLink(destination: menuItems[index].title == "ABC" ?
FirstList() :
SecondView(menuItem: menuItems[index])) {
HomeRow(menuItem: menuItems[index])
}
}
}
}
}
}
struct HomeRow: View {
var menuItem: MenuItem
var body: some View {
HStack {
Text(verbatim: menuItem.title)
}
}
}
struct FirstList: View {
var body: some View {
List(1 ..< 5) { index in
Text("Row \(index)")
}
.listStyle(GroupedListStyle())
}
}
struct SecondView: View {
var menuItem: MenuItem
var body: some View {
Text(menuItem.title)
}
}
However, I get the following error with my NavigationLink:
Result values in '? :' expression have mismatching types 'FirstList'
and 'SecondView'
Since my goal here is to have two different views I point to based upon the title, I'd like to find some way to make that work.
The answer posted by superpuccio seems to be pretty close to what I want, but with the expected complexity of the target views, I do not think it would be feasible to compose them entirely within NavigationLink.
Since you have a dynamic List I suggest you use a ForEach inside a List this way:
import SwiftUI
struct MenuItem {
let title: String
let isEnabled: Bool
}
struct HomeList: View {
let menuItems = [
MenuItem(title: "ABC", isEnabled: true),
MenuItem(title: "DEF", isEnabled: false),
MenuItem(title: "GHI", isEnabled: true)
]
var body: some View {
let firstRowModel = menuItems[0]
let actualModel = menuItems[1...menuItems.count-1]
return NavigationView {
List {
NavigationLink(destination: FirstList()) {
HomeRow(menuItem: firstRowModel)
}
ForEach(actualModel.indices, id: \.self) { index in
NavigationLink(destination: SecondView(menuItem: actualModel[index])) {
HomeRow(menuItem: actualModel[index])
}
}
}
}
}
}
struct HomeRow: View {
var menuItem: MenuItem
var body: some View {
HStack {
Text(verbatim: menuItem.title)
}
}
}
struct FirstList: View {
var body: some View {
List(1 ..< 5) { index in
Text("Row \(index)")
}
.listStyle(GroupedListStyle())
}
}
struct SecondView: View {
var menuItem: MenuItem
var body: some View {
Text(menuItem.title)
}
}
I would include the condition in the destination.
var body: some View {
NavigationView {
List(1 ..< 5) { idx in
NavigationLink(destination:
idx < 3 ? Text("1111") : Text("2222") ) {
Text("Row \(idx)")
}
}
.listStyle(GroupedListStyle())
}
}