how to implement move on a grouped list in swiftui - swiftui

how do i implement the onMove function in a grouped list? unfortunately i cannot see where i get the "to company" information from...
here is the code:
import SwiftUI
struct Person: Identifiable, Hashable {
var id = UUID()
var name: String
}
struct Company : Identifiable, Hashable {
var id = UUID()
var name: String
var employees : [Person]
}
class CompanyList: ObservableObject {
#Published var companies = [
Company(name: "Apple", employees: [Person(name:"Bob"), Person(name:"Brenda")]),
Company(name: "Microsoft", employees: [Person(name:"Bill"), Person(name:"Lucas")]),
Company(name: "Facebook", employees: [Person(name:"Mark"), Person(name:"Sandy")])
]
func deleteListItem(whichElement: IndexSet, from company: Company) {
let index = companies.firstIndex(of: company)!
companies[index].employees.remove(atOffsets: whichElement)
}
func moveListItem(whichElement: IndexSet, to companyIndex: Int) {
print(whichElement.first)
if whichElement.count > 1 {
whichElement.dropFirst()
print(whichElement.first)
}
print(companyIndex)
// let employee = companies[index].employees[whichElement.first!]
// companies[index].employees.remove(at: whichElement.first!)
//
}
}
struct ContentView: View {
#ObservedObject var companyList = CompanyList()
#State var text : String = ""
var body: some View {
NavigationView {
VStack {
List () {
ForEach (companyList.companies, id: \.self) { company in
Section(header: Text(company.name)) {
ForEach(company.employees) { employee in
Text(employee.name).id(UUID())
}
.onDelete { (indexSet) in
self.companyList.deleteListItem(whichElement: indexSet, from: company)
}
.onMove { indexSet, intValue in
self.companyList.moveListItem(whichElement: indexSet, to: intValue)
}
.onInsert(of: ["chris"]) { (intValue, _) in
print("wtf")
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarItems(trailing: EditButton())
}
}
}
}

There is 2 main changes and 1 swiftUI drawback.
Updated the method moveListItem
Created a alternative to change companies by moving with a NavigationalLink
SwiftUI have no option to move between groups in GroupedList with .onMove()
The app works technically. But not as you intended unless Apple add the feature. There is one more option, that is to create custom list view with custom move method, which is entirely a different topic.
import SwiftUI
struct Person: Identifiable, Hashable {
var id = UUID()
var name: String
}
struct Company : Identifiable, Hashable {
var id = UUID()
var name: String
var employees : [Person]
}
class CompanyList: ObservableObject {
#Published var companies = [
Company(name: "Apple", employees: [Person(name:"Bob"), Person(name:"Brenda")]),
Company(name: "Microsoft", employees: [Person(name:"Bill"), Person(name:"Lucas")]),
Company(name: "Facebook", employees: [Person(name:"Mark"), Person(name:"Sandy")])
]
func deleteListItem(whichElement: IndexSet, from company: Company) {
if let index = self.companies.firstIndex(of: company) {
self.companies[index].employees.remove(atOffsets: whichElement)
}
}
func moveListItem(whichElement: IndexSet, to companyIndex: Int, from company: Company) {
if let index = self.companies.firstIndex(of: company) {
self.companies[index].employees.move(fromOffsets: whichElement, toOffset: companyIndex)
}
}
}
struct TestView: View {
#EnvironmentObject var companyList: CompanyList
#State var text : String = ""
var body: some View {
NavigationView {
VStack {
List () {
ForEach (companyList.companies, id: \.self) { company in
Section(header: Text(company.name)) {
ForEach(company.employees) { employee in
NavigationLink(destination: EditEmployee(company: company, employee: employee)){
Text(employee.name)
}.id(UUID())
}
.onDelete { (indexSet) in
self.companyList.deleteListItem(whichElement: indexSet, from: company)
}
.onMove { indexSet, intValue in
self.companyList.moveListItem(whichElement: indexSet, to: intValue, from: company)
}
.onInsert(of: ["chris"]) { (intValue, _) in
print("wtf")
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarItems(trailing: EditButton())
}
}
}
}
struct EditEmployee: View {
#EnvironmentObject var companyList: CompanyList
var company: Company
var employee: Person
var body: some View {
VStack(alignment: .leading) {
Text("Company")
Picker(selection: Binding<Company>(
get: { () -> Company in
return self.company
}, set: { (company) in
if let cid = self.companyList.companies.firstIndex(of: self.company) {
if let eid = self.companyList.companies[cid].employees.firstIndex(of: self.employee) {
if let ncid = self.companyList.companies.firstIndex(of: company) {
self.companyList.companies[cid].employees.remove(at: eid)
self.companyList.companies[ncid].employees.append(self.employee)
}
}
}
}
), label: Text("")){
ForEach(self.companyList.companies) { company in
Text(company.name).tag(company)
}
}.pickerStyle(SegmentedPickerStyle())
Spacer()
}.padding()
.navigationBarTitle(self.employee.name)
}
}

Related

Keeping the original datasource in sync with downstream bindings

If I have a collection of fruits, and I pass one of them to a detail view, how do I edit that item so that both the item and it's original datasource are updated?
final class Merchant: ObservableObject {
#Published
var selection: Fruit?
#Published
var fruits = [
Fruit(name: "Banana"),
Fruit(name: "Apple")
]
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
merchant.selection = fruit
} label: {
Text(fruit.name)
}
.buttonStyle(.borderedProminent)
}
}
.sheet(item: $merchant.selection, content: {
FruitDetailView(item: $0)
})
}
}
struct FruitDetailView: View {
let item: Fruit
init(item: Fruit) {
self.item = item
}
var body: some View {
VStack {
Text(item.name)
Button("Press Me") {
item.name = "Watermelon" // error
}
}
}
}
Changing the item on FruitDetailView to a binding doesn't change the original datasource.
There are a number of ways to achieve what you ask. This is one simple way using the model constructs you already have. It uses the Merchant selection to update the Merchant fruits data.
struct ContentView: View {
#StateObject var merchant = Merchant()
var body: some View {
FruitsView().environmentObject(merchant)
}
}
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
final class Merchant: ObservableObject {
#Published var selection: Fruit? = nil {
didSet {
if selection != nil,
let index = fruits.firstIndex(where: {$0.id == selection!.id}) {
fruits[index].name = selection!.name
}
}
}
#Published var fruits = [Fruit(name: "Banana"), Fruit(name: "Apple")]
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
merchant.selection = fruit
} label: {
Text(fruit.name)
}.buttonStyle(.borderedProminent)
}
}
.sheet(item: $merchant.selection) { _ in
FruitDetailView().environmentObject(merchant)
}
}
}
struct FruitDetailView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
Text(merchant.selection?.name ?? "no selection name")
Button("Press Me") {
merchant.selection?.name = "Watermelon"
}
}
}
}
EDIT-1:
This is another way of keeping the model in sync. It uses a function updateFruits in the Merchant ObservableObject class, to update the model's data.
It separates the UI interaction part using a local #State var selection: Fruit? from the main data in the Merchant model.
final class Merchant: ObservableObject {
#Published var fruits = [Fruit(name: "Banana"), Fruit(name: "Apple")]
func updateFruits(with item: Fruit) {
if let index = fruits.firstIndex(where: {$0.id == item.id}) {
fruits[index].name = item.name
}
}
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
#State var selection: Fruit?
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
selection = fruit
} label: {
Text(fruit.name)
}.buttonStyle(.borderedProminent)
}
}
.sheet(item: $selection) { item in
FruitDetailView(item: item).environmentObject(merchant)
}
}
}
struct FruitDetailView: View {
#EnvironmentObject var merchant: Merchant
#State var item: Fruit
var body: some View {
VStack {
Text(item.name)
Button("Press Me") {
item.name = "Watermelon"
merchant.updateFruits(with: item)
}
}
}
}

List row reset value

I have a List, with custom Stepper inside each row. Therefore, when I scroll my stepper is reset. (The increment and decrement works when is visible. When it disappear, it's reset. Don't keep the state. It's alway's reset).
Xcode: v14.2 / Simulator iOS: 16.2
struct Product: Codable, Hashable, Identifiable {
let id: String
let name: String
let step: Int
let quantity: Int
let priceHT: Double
}
class ProductViewModel: ObservableObject {
#Published var products = [Product]()
...
}
struct ProductListView: View {
#EnvironmentObject var productViewModel: ProductViewModel
var body: some View {
List(productViewModel.products) { product in
ProductRowView(product: product)
  }
}
}
My List row:
I tried to modify #State with #binding, but without success.
struct ProductRowView: View {
#State var product: Product
var body: some View {
HStack {
VStack {
Text(product.name)
Text(String(format: "%.2f", product.priceHT) + "€ HT")
}
Spacer()
MyStepper(product: $product, value: product.quantity)
.font(.title)
}
}
}
My Custom stepper:
struct MyStepper: View {
#Binding var product: Product
#State var value: Int = 0
var body: some View {
HStack {
VStack() {
HStack {
Button(action: {
value -= product.step
if let row = orderViewModel.productsOrder.firstIndex(where: { $0.name == product.name }) {
let order = Product(id: product.id, name: product.name, step: product.step, quantity: value, priceHT: product.priceHT)
if (value == 0) {
orderViewModel.productsOrder.remove(at: row)
} else {
orderViewModel.productsOrder[row] = order
}
}
}, label: {
Image(systemName: "minus.square.fill")
})
Text(value.formatted())
Button(action: {
value += product.step
let order = Product(id: product.id, name: product.name, step: product.step, quantity: value, priceHT: product.priceHT)
if let row = orderViewModel.productsOrder.firstIndex(where: { $0.name == product.name }) {
orderViewModel.productsOrder[row] = order
} else {
orderViewModel.productsOrder.append(order)
}
}, label: {
Image(systemName: "plus.app.fill")
})
}
Text(product.unit)
}
}
}
}
Thks
EDIT / RESOLVED
Here is the solution for my case :
Change type of quantity. let to var
struct Product: Codable, Hashable, Identifiable {
...
var quantity: Int
...
}
Delete #State in MyStepper and replace value by product.quantity
Use bindings for that, e.g.
struct ProductListView: View {
#EnvironmentObject var model: Model
var body: some View {
List($model.products) { $product in
ProductRowView(product: $product)
}
}
}
struct ProductRowView: View {
#Binding var product: Product // now you have write access to the Product struct
...
However, to make the View reusable and to help with previewing, it's best to pass in only the simple types the View needs, e.g.
struct TitlePriceView: View {
let title: String
#Binding var price: Double
// etc.
TitlePriceView(title: product.title, price: $product.price)

SwiftUI - Binding var updates the original value only the first time, and then it doesn't update anymore

I have a Picker that updates a #Binding value, that is linked to the original #State value. The problem is that it gets updated only the first time I change it, and then it always remains like that. Not only in the sheet, but also in the ContentView, which is the original view in which the #State variable is declared. Here's a video showing the problem, and here's the code:
ContentView:
struct ContentView: View {
#State var cityForTheView = lisbon
#State var showCitySelection = false
var body: some View {
VStack {
//View
}
.sheet(isPresented: $showCitySelection) {
MapView(cityFromCV: $cityForTheView)
}
}
}
MapView:
struct MapView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var cityFromCV : CityModel
#State var availableCities = [String]()
#State var selectedCity = "Lisbon"
var body: some View {
NavigationView {
ZStack {
VStack {
Form {
HStack {
Text("Select a city:")
Spacer()
Picker("", selection: $selectedCity) {
ForEach(availableCities, id: \.self) {
Text($0)
}
}
.accentColor(.purple)
}
.pickerStyle(MenuPickerStyle())
Section {
Text("Current city: \(cityFromCV.cityName)")
}
}
}
}
}
.interactiveDismissDisabled()
.onAppear {
availableCities = []
for ct in cities {
availableCities.append(ct.cityName)
}
}
.onChange(of: selectedCity) { newv in
self.cityFromCV = cities.first(where: { $0.cityName == newv })!
}
}
}
CityModel:
class CityModel : Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var cityName : String
var country : String
var imageName : String
init(cityName: String, country: String, imageName : String) {
self.cityName = cityName
self.country = country
self.imageName = imageName
}
static func == (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
static func < (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
}
var cities = [
lisbon,
CityModel(cityName: "Madrid", country: "Spain", imageName: "Madrid"),
CityModel(cityName: "Barcelona", country: "Spain", imageName: "Barcelona"),
CityModel(cityName: "Paris", country: "France", imageName: "Paris")
]
var lisbon = CityModel(cityName: "Lisbon", country: "Portugal", imageName: "Lisbon")
What am I doing wrong?
The problem are:
at line 35: you use selectedCity to store the selected data from your picker.
Picker("", selection: $selectedCity) {
then at your line 46: you use cityFromCV.cityName to display the data which will always show the wrong data because you store your selected data in the variable selectedCity.
Section {
Text("Current city: \(cityFromCV.cityName)")
}
Solution: Just change from cityFromCV.cityName to selectedCity.

SwiftUI Catch the Picker value from another view

I need to add searchbar and nil value to picker. I want to create a custom picker and use it everywhere to avoid code repetition. I was able to create a picker using this and this code, but I could not find how to capture the value I chose in another view.
This code helps me search inside the picker.
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.autocapitalizationType = .none
searchBar.searchBarStyle = .minimal
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
}
This is the custom picker I created.
struct CustomPicker<Item>: View where Item: Hashable {
let items: [Item]
let title: String
let text: KeyPath<Item, String>
let needOptional: Bool
let needSearchBar: Bool
#State var item: Item? = nil
#State var searchText: String = ""
var filteredItems: [Item] {
items.filter {
searchText.isEmpty ? true : $0[keyPath: text].lowercased().localizedStandardContains(searchText.lowercased())
}
}
var body: some View {
Picker(selection: $item, label: Text(title)) {
if needSearchBar {
SearchBar(text: $searchText, placeholder: "Search")
.padding(.horizontal, -12)
}
if needOptional {
Text("[none]").tag(nil as Item?)
.foregroundColor(.red)
}
ForEach(filteredItems, id: \.self) { item in
Text(item[keyPath: text])
.tag(item as Item?)
}
}
.onAppear{
searchText = ""
}
}
}
Usage
Country Model Example
struct Country: Codable, Identifiable, Hashable {
let id = UUID()
let name: String
enum CodingKeys: String, CodingKey {
case id, name
}
}
in ContentView
Form {
CustomPicker(items: countryArray, title: "Country", text: \Country.name, needOptional: true, needSearchBar: true)
}
How do I catch the selected Item(optional) in ContentView?
Use #Binding to bind your item.
struct CustomPicker<Item>: View where Item: Hashable {
let items: [Item]
let title: String
let text: KeyPath<Item, String>
let needOptional: Bool
let needSearchBar: Bool
#Binding var item: Item? // <--Here
#State var searchText: String = ""
//-----Other code---------//
And use #State here in content view for getting selected value.
struct ContentView: View {
#State private var selectedItem: Country? //<-- Here
private let countryArray: [Country] = [.init(name: "A"), .init(name: "B")]
var body: some View {
Form {
CustomPicker(items: countryArray, title: "Country", text: \Country.name, needOptional: true, needSearchBar: true, item: $selectedItem) // <-- Here
if let selectedItem = selectedItem {
Text(selectedItem.name)
}
}
}
}

Why .append doesn't reload swiftUI view

I have the following view hierarchy
Nurse List View > Nurse Card > Favorite button
Nurse List View
struct NurseListView: View {
#State var data: [Nurse] = []
var body: some View {
List {
ForEach(data.indices, id: \.self) { index in
NurseCard(data: self.$data[index])
}
}
}
}
Nurse Card
struct NurseCard: View {
#Binding var data: Nurse
var body: some View {
FavoriteActionView(data:
Binding(
get: { self.data },
set: { self.data = $0 as! Nurse }
)
)
}
}
Favorite Action View
struct FavoriteActionView: View {
#Binding var data: FavoritableData
var body: some View {
Button(action: {
self.toggleFavIcon()
}) {
VStack {
Image(data.isFavorite ? "fav-icon" : "not-fav-icon")
Text(String(data.likes.count))
}
}
}
private func toggleFavIcon() {
if data.isFavorite {
if let index = data.likes.firstIndex(of: AppState.currentUser.uid) {
data.likes.remove(at: index)
}
} else {
data.likes.append(AppState.currentUser.uid)
}
}
}
When toggleFavIcon execute, it append/remove the user id from the likes property in data object but I can't see the change unless I go back to previous page and reopen the page. What I am missing here?
As Asperi wrote, using an ObservableObject would work well here. Something like this:
class FavoritableData: ObservableObject {
#Published var likes: [String] = []
#Published var isFavorite = false
}
struct FavoriteActionView: View {
#ObservedObject var data: FavoritableData
var body: some View {
Button(action: {
self.toggleFavIcon()
}) {
VStack {
Image(data.isFavorite ? "fav-icon" : "not-fav-icon")
Text(String(data.likes.count))
}
}
}
private func toggleFavIcon() {
if data.isFavorite {
if let index = data.likes.firstIndex(of: AppState.currentUser.uid) {
data.likes.remove(at: index)
}
} else {
data.likes.append(AppState.currentUser.uid)
}
}
}