SwuiftUI Custom Sorting - ios

I'm just starting out in SwuiftUI so bear with me.
I have a Game stuct that has a field lastUpdated and title. I want to have the choice to sort by my list by lastUpdated or title. But I'm not sure how this works. I've looked into sorted(by:) but I can't really get anything to work. Suggestions?
struct Game: Identifiable, Codable {
let id: UUID
var title: String
var players: [Player]
var lastUsed: Date }
GameView
struct GameListView: View {
#Binding var games: [Game]
var body: some View {
List {
ForEach($games) { $game in
NavigationLink(destination: GameView(game: $game)) {
Text(game.title)}
}
}

The scenario is slightly complicated by the Binding form of ForEach, but you should still be able to return a sorted collection. It might look like this:
struct GameListView: View {
#Binding var games: [Game]
#State var sortType : SortType = .lastUsed
enum SortType {
case lastUsed, title
}
var sortedGames : [Binding<Game>] {
$games.sorted(by: { a, b in
switch sortType {
case .lastUsed:
return a.wrappedValue.lastUsed < b.wrappedValue.lastUsed
case .title:
return a.wrappedValue.title < b.wrappedValue.title
}
})
}
var body: some View {
List {
ForEach(sortedGames) { $game in
NavigationLink(destination: GameView(game: $game)) {
Text(game.title)}
}
}
}
}

Related

SwiftUI Bind to #ObservableObject in array

How do I pass a bindable object into a view inside a ForEach loop?
Minimum reproducible code below.
class Person: Identifiable, ObservableObject {
let id: UUID = UUID()
#Published var healthy: Bool = true
}
class GroupOfPeople {
let people: [Person] = [Person(), Person(), Person()]
}
public struct GroupListView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
//MARK: Other properties
let group: GroupOfPeople = GroupOfPeople()
//MARK: Body
public var body: some View {
ForEach(group.people) { person in
//ERROR: Cannot find '$person' in scope
PersonView(person: $person)
}
}
//MARK: Init
}
public struct PersonView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
#Binding var person: Person
//MARK: Other properties
//MARK: Body
public var body: some View {
switch person.healthy {
case true:
Text("Healthy")
case false:
Text("Not Healthy")
}
}
//MARK: Init
init(person: Binding<Person>) {
self._person = person
}
}
The error I get is Cannot find '$person' in scope. I understand that the #Binding part of the variable is not in scope while the ForEach loop is executing. I'm looking for advice on a different pattern to accomplish #Binding objects to views in a List in SwiftUI.
The SwiftUI way would be something like this:
// struct instead of class
struct Person: Identifiable {
let id: UUID = UUID()
var healthy: Bool = true
}
// class publishing an array of Person
class GroupOfPeople: ObservableObject {
#Published var people: [Person] = [
Person(), Person(), Person()
]
}
struct GroupListView: View {
// instantiating the class
#StateObject var group: GroupOfPeople = GroupOfPeople()
var body: some View {
List {
// now you can use the $ init of ForEach
ForEach($group.people) { $person in
PersonView(person: $person)
}
}
}
}
struct PersonView: View {
#Binding var person: Person
var body: some View {
HStack {
// ternary instead of switch
Text(person.healthy ? "Healthy" : "Not Healthy")
Spacer()
// Button to change, so Binding makes some sense :)
Button("change") {
person.healthy.toggle()
}
}
}
}
You don't need Binding. You need ObservedObject.
for anyone still wondering... it looks like this has been added
.onContinuousHover(perform: { phase in
switch phase {
case .active(let location):
print(location.x)
case .ended:
print("ended")
}
})

SwiftUI - Should you use `#State var` or `let` in child view when using ForEach

I think I've a gap in understanding what exactly #State means, especially when it comes to displaying contents from a ForEach loop.
My scenario: I've created minimum reproducible example. Below is a parent view with a ForEach loop. Each child view has aNavigationLink.
// Parent code which passes a Course instance down to the child view - i.e. CourseView
struct ContentView: View {
#StateObject private var viewModel: ViewModel = .init()
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor) {
CourseView(course: course, viewModel: viewModel)
}
}
}
}
}
}
class ViewModel: ObservableObject {
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
}
struct Course: Identifiable {
var id: String = UUID().uuidString
var name: String
var instructor: String
}
Actual Dilemma: I've tried two variations for the CourseView, one with let constant and another with a #State var for the course field. Additional comments in the code below.
The one with the let constant successfully updates the child view when the navigation link is open. However, the one with #State var doesn't update the view.
struct CourseView: View {
// Case 1: Using let constant (works as expected)
let course: Course
// Case 2: Using #State var (doesn't update the UI)
// #State var course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Edit Instructor", action: editInstructor)
}
}
// Case 1: It works and UI gets updated
// Case 2: Doesn't work as is.
// I've to directly update the #State var instead of updating the clone -
// which sometimes doesn't update the var in my actual project
// (that I'm trying to reproduce). It definitely works here though.
private func editInstructor() {
let instructor = course.instructor == "Bob" ? "John" : "Bob"
var course = course
course.instructor = instructor
save(course)
}
// Simulating a database save, akin to something like GRDB
// Here, I'm just updating the array to see if ForEach picks up the changes
private func save(_ courseToSave: Course) {
guard let index = viewModel.courses.firstIndex(where: { $0.id == course.id }) else {
return
}
viewModel.courses[index] = courseToSave
}
}
What I'm looking for is the best practice for a scenario where looping through an array of models is required and the model is updated in DB from within the child view.
Here is a right way for you, do not forget that we do not need put logic in View! the view should be dummy as possible!
struct ContentView: View {
#StateObject private var viewModel: ViewModel = ViewModel.shared
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor, destination: CourseView(course: course, viewModel: viewModel))
}
}
}
}
}
struct CourseView: View {
let course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Update Instructor", action: { viewModel.update(course) })
}
}
}
class ViewModel: ObservableObject {
static let shared: ViewModel = ViewModel()
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
func update(_ course: Course) {
guard let index = courses.firstIndex(where: { $0.id == course.id }) else {
return
}
courses[index] = Course(name: course.name, instructor: (course.instructor == "Bob") ? "John" : "Bob")
}
}
struct Course: Identifiable {
let id: String = UUID().uuidString
var name: String
var instructor: String
}

Passing #Binding variable from protocol var to if in SwiftUI

I have a model like this:
protocol PurchasableProduct {
var randomId: String { get }
}
class Cart: Identifiable {
var items: [PurchasableProduct]
init(items: [PurchasableProduct]) {
self.items = items
}
}
class Product: Identifiable, PurchasableProduct {
var randomId = UUID().uuidString
var notes: String = ""
}
class DigitalGood: Identifiable, PurchasableProduct {
var randomId = UUID().uuidString
}
where items conform to protocol PurchasableProduct.
I want to build a View that shows cart like this:
struct CartView: View {
#State var cart: Cart
var body: some View {
List {
ForEach(cart.items.indices) { index in
CartItemView(item: self.$cart.items[index])
}
}
}
}
where CartItemView is:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var body: some View {
VStack {
if self.item is Product {
Text("Product")
} else {
Text("Digital Good")
}
}
}
}
That's working and give me result as
This (screenshot)
But I want to extend this a but more that my items element can be passed as a binding variable lets say as:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var body: some View {
VStack {
if self.item is Product {
VStack {
TextField("add notes", text: (self.$item as! Product).notes) // ❌ Cannot convert value of type 'String' to expected argument type 'Binding<String>'
TextField("add notes", text: (self.$item as! Binding<Product>).notes) // ⚠️ Cast from 'Binding<PurchasableProduct>' to unrelated type 'Binding<Product>' always fails
}
} else {
Text("Digital Good")
}
}
}
}
What I'm trying to achieve is:
I have a collection of items that depends on a class should be drawn differently
Items have different editable sync that should be binded into CartView
Not sure if thats syntax issue or my approach issue ... how to cast this on body to get the correct view based on type?
You may create a custom binding:
struct CartItemView: View {
#Binding var item: PurchasableProduct
var product: Binding<Product>? {
guard item is Product else { return nil }
return .init(
get: {
self.$item.wrappedValue as! Product
}, set: {
self.$item.wrappedValue = $0
}
)
}
var body: some View {
VStack {
if product != nil {
TextField("add notes", text: product!.notes)
} else {
Text("Digital Good")
}
}
}
}

Array of binding variables for multiple toggles in MVVM pattern in SwiftUI

I have a simple use case of having a VStack of a dynamic number of Text with Toggle buttons coming from an array.
import SwiftUI
public struct Test: View {
#ObservedObject public var viewModel = TestViewModel()
public init() {
}
public var body: some View {
VStack {
ForEach(viewModel.models) { model in
ToggleView(title: <#T##Binding<String>#>, switchState: <#T##Binding<Bool>#>)
//how to add the above
}
}.padding(50)
}
}
struct ToggleView: View {
#Binding var title: String
#Binding var switchState: Bool
var body: some View {
VStack {
Toggle(isOn: $switchState) {
Text(title)
}
}
}
}
public class TestModel: Identifiable {
#Published var state: Bool {
didSet {
//do something
//publish to the view that the state has changed
}
}
#Published var title: String
init(state: Bool, title: String) {
self.state = state
self.title = title
}
}
public class TestViewModel: ObservableObject {
#Published var models: [TestModel] = [TestModel(state: false, title: "Title 1"), TestModel(state: true, title: "Title 2")]
}
The following questions arise:
In MVVM pattern, is it ok to have the binding variables in model class or should it be inside the view model?
How to send the message of state change from model class to view/scene when the toggle state is changed?
If using an array of binding variables in view model for each of the toggle states, how to know which particular element of array has changed? (see the following code snippet)
class ViewModel {
#Published var dataModel: [TestModel]
#Published var toggleStates = [Bool]() {
didSet {
//do something based on which element of the toggle states array has changed
}
}
}
Please help with the above questions.
here is one way you could achieve what you desire.
Has you will notice you have to use the binding power of #ObservedObject.
The trick is to use indexes to reach the array elements for you binding.
If you loop on the array elements model directly you loose the underlying binding properties.
struct Test: View {
#ObservedObject public var viewModel = TestViewModel()
var body: some View {
VStack {
ForEach(viewModel.models.indices) { index in
ToggleView(title: self.viewModel.models[index].title, switchState: self.$viewModel.models[index].state)
}
}.padding(50)
}
}
class TestViewModel: ObservableObject {
#Published var models: [TestModel] = [
TestModel(state: false, title: "Title 1"),
TestModel(state: true, title: "Title 2")]
}
struct ToggleView: View {
var title: String
#Binding var switchState: Bool
var body: some View {
VStack {
Toggle(isOn: $switchState) {
Text(title)
}
}
}
}
class TestModel: Identifiable {
var state: Bool
var title: String
init(state: Bool, title: String) {
self.title = title
self.state = state
}
}
Hope this does the trick for you.
Best

SwiftUI Picker in Form does not show checkmark

I have a Picker embedded in Form, however I can't get it working that it shows a checkmark and the selected value in the form.
NavigationView {
Form {
Section {
Picker(selection: $currencyCode, label: Text("Currency")) {
ForEach(0 ..< codes.count) {
Text(self.codes[$0]).tag($0)
}
}
}
}
}
TL;DR
Your variable currencyCode does not match the type of the ID for each element in your ForEach. Either iterate over the codes in your ForEach, or pass your Picker an index.
Below are three equivalent examples. Notice that the #State variable which is passed to Picker always matches the ID of element that the ForEach iterates over:
Also note that I have picked a default value for the #State variable which is not in the array ("", -1, UUID()), so nothing is shown when the form loads. If you want a default option, just make that the default value for your #State variable.
Example 1: Iterate over codes (i.e. String)
struct ContentView: View {
#State private var currencyCode: String = ""
var codes: [String] = ["EUR", "GBP", "USD"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $currencyCode, label: Text("Currency")) {
// ID is a String ----v
ForEach(codes, id: \.self) { (string: String) in
Text(string)
}
}
}
}
}
}
}
Example 2: Iterate over indices (i.e. Int)
struct ContentView: View {
#State private var selectedIndex: Int = -1
var codes: [String] = ["EUR", "GBP", "USD"]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedIndex, label: Text("Currency")) {
// ID is an Int --------------v
ForEach(codes.indices, id: \.self) { (index: Int) in
Text(self.codes[index])
}
}
}
}
}
}
}
Example 3: Iterate over an identifiable struct by its ID type (i.e. UUID)
struct Code: Identifiable {
var id = UUID()
var value: String
init(_ value: String) {
self.value = value
}
}
struct ContentView: View {
#State private var selectedUUID = UUID()
var codes = [Code("EUR"), Code("GBP"), Code("USD")]
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedUUID, label: Text("Currency")) {
// ID is a UUID, because Code conforms to Identifiable
ForEach(self.codes) { (code: Code) in
Text(code.value)
}
}
}
}
}
}
}
It's difficult to say, what you are doing wrong, because your example doesn't include the declaration of codes or currencyCode. I suspect that the problem is with the binding being of a different type than the tag you are setting on a picker (which is an Int in your case).
Anyway, this works:
struct ContentView: View {
let codes = Array(CurrencyCode.allCases)
#State private var currencyCode: CurrencyCode?
var body: some View {
NavigationView {
Form {
Section {
Picker("Currency",
selection: $currencyCode) {
ForEach(codes, id: \.rawValue) {
Text($0.rawValue).tag(Optional<CurrencyCode>.some($0))
}
}
}
}
}
}
}
enum CurrencyCode: String, CaseIterable {
case eur = "EUR"
case gbp = "GBP"
case usd = "USD"
}

Resources