UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Is there a way update rows dynamically

Forums > SwiftUI

I can update the count and it increases in view as you click. My sort then re-orders them after I click on it again, but was wondering if the cell row can move up dynamically as the clic count increases.

struct Family: Identifiable {
    let id = UUID()
    let name: String
    let age: Int
    var on = false
    var clicks: Int
}
class FamilyViewModel: Identifiable {
    var member: Family

    init(member: Family) {
        self.member = member
    }

    var name: String {
        member.name
    }

    var age: Int {
        member.age
    }

    var on: Bool {
        member.on
    }

    var clicks: Int {
        member.clicks
    }

    var famColors: Color {
        switch member.name {
        case "David":
            return .red
        case "Anna":
            return .green
        case "Nicholas":
            return .blue
        case "Alexandra":
            return .purple
        default:
            return .white
        }
    }

    func updateOn() {
        member.on.toggle()
    }

    func updateClickCount() {
        member.clicks += 1
    }
}
class FamilyListViewModel: ObservableObject {
    var members = [FamilyViewModel]()
    @Published var sortedMembers: [FamilyViewModel] = []

    var showCount: Int {
        members.count
    }

    init() {
        loadFamily()
        sortMembers(index: 0)
    }

    func sortMembers(index: Int) {
        switch index {
        case 0:
            sortedMembers = members.sorted(by: { $0.name < $1.name })
        case 1:
            sortedMembers = members.sorted(by: { $0.clicks > $1.clicks })
        case 2:
            sortedMembers = members.sorted(by: { $0.age < $1.age })
        default:
            sortedMembers = members
        }
    }

    func loadFamily() {
        let fam1 = FamilyViewModel(member: Family(name: "David", age: 52, on: false, clicks: 0))
        let fam2 =  FamilyViewModel(member: Family(name: "Anna", age: 50, on: false, clicks: 0))
        let fam3 =  FamilyViewModel(member: Family(name: "Nicholas", age: 20, on: false, clicks: 0))
        let fam4 =  FamilyViewModel(member: Family(name: "Alexandra", age: 18, on: false, clicks: 0))
        members.append(contentsOf: [fam1, fam2, fam3, fam4])
    }

    func delete(at offsets: IndexSet) {
        sortedMembers.remove(atOffsets: offsets)
    }
}
struct ContentView: View {
    // MARK: - PROPERTIES
    @ObservedObject var vm = FamilyListViewModel()
    @State private var selectedItem = 0

    // MARK: - BODY
    var body: some View {
        NavigationView {
            VStack {
                List {
                    Text("Members Count: \(vm.showCount)")
                    Picker("", selection: $selectedItem) { 
                        Text("Name").tag(0)
                        Text("Count").tag(1)
                        Text("Age").tag(2)
                    } // END:PICKER
                    .pickerStyle(SegmentedPickerStyle())
                    .padding(.horizontal)
                    .onChange(of: selectedItem, perform: { (value) in
                        vm.sortMembers(index: value)
                    }) // END:ONCHANGE
                    ForEach(vm.sortedMembers, id: \.id) { member in
                        CellView(family: member, sort: $selectedItem)
                    } // END:FOREACH
                    .onDelete { IndexSet in
                        vm.delete(at: IndexSet)
                    } // END:ONDELETE
                } // END:LIST
                .navigationTitle("Family List")
            } // END:VSTACK
        } // END:NAVVIEW
        .navigationViewStyle(.stack)
    }
}
struct CellView: View {
    // MARK: - PROPERTIES
    // NEED TO ADD THIS TO HAVE VIEW UPDATE
    @ObservedObject var vm = FamilyListViewModel()
    let family: FamilyViewModel
    @Binding var sort: Int

    // MARK: - BODY
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Image(systemName: family.on ? "circle.fill" : "circle")
                    .foregroundColor(family.on ? family.famColors : .white)
                    .onTapGesture {
                        family.updateOn()
                        family.updateClickCount()
                        vm.sortMembers(index: sort)
                    }
                Text(family.name)
                    .foregroundColor(family.famColors)
            } // END:HSTACK
            HStack {
                Spacer()
                Text("Click Count: \(family.clicks)")
                    .font(.system(size: 12))
                Text("Age: \(family.age)")
                    .font(.system(size: 12))
            } // END:HSTACK
        } // END:VSTACK
    }
}

1      

David asks:

I was wondering if the cell row can move up dynamically as the click count increases.

Yes!

Think of your application's architecture. You probably have:

  1. A family member model. (name, age, clickCount)
  2. A family view (Show a list of everyone in a family. Options for sorting.)
  3. A member view (One ROW of your list. Just show details of ONE family member.)

If you've watched the videos on Model-View-ViewModel, you'll see what's missing is the ViewModel.
You'll need another file to hold your application's business rules. I think some developers call these The Intentions.

The Intentions

Think of what you want your family view to do. Write them down. Tinker. Doodle. Scratch out, review, and revise. These are your family view's intentions. Now get serious. You do NOT want to embed these rules into your view. Think of your view as a display case in a bakery. You just want to show the finished products, all tasty and lickable. You do not want the case full of flour, egg shells, spoons, flavour extracts, etc.

View Model

Your application's business rules and intentions should be in your ViewModel. What did you decide your family view should do? WIth just the code you provided, I came up with this short list.

  1. Load a family with family members.
  2. Sort the family members by age, name, or click value.
  3. Publish a list of family members for display.
  4. Notify views when the list of family members change, so that they may redraw themselves.

But wait! What about clicking on a family member? Well that deserves a view model for the Member view! Tapping a member should update that member's click value. Consider creating a view model for each view you have in your application. Decide on all the view's intentions and write them in the view model.

Member Model

This is a dead simple model for one member of a family. Because the member view only has one intention, I included it in this file.

//
//  Dead Simple Member Model
//  Created by Obelix on 2022.02.14
//
import SwiftUI

class Member : Identifiable, ObservableObject  {
    let id                    = UUID()
    let name                  : String
    let age                   : Int
    @Published var clickCount = 0  // broadcast changes

    var memberColor: Color {
        switch name {
        case "David":
            return .red
        case "Anna":
            return .green
        case "Nicholas":
            return .blue
        case "Alexandra":
            return .purple
        case "Obelix":
            return .indigo
        case "Taylor":
            return .cyan
        default:
            return .black
        }
    }
    // Not all struct's vars have defaults.
    init( name: String, age: Int, clickCount: Int = 0) {
        self.name       = name
        self.age        = age
        self.clickCount = clickCount
    }
    // Intentions (Should be in ViewModel) -----------------
    func updateClick() { clickCount += 1 }
    // For UI Testing.
    static let example = Member(name: "Taylor", age: 22)
}

Member View

This is a view to only show ONE member. Notice the member view has one purpose. It shows one member. It doesn't know anything about sorting, or the family it is in.

// MemberView
// Created by Obelix on 2022.02.14
//
import SwiftUI
// Design this view to display ONE family member. That's it!
// This view should NOT know about a family or other members.
struct MemberView: View {
    @ObservedObject var selectedMember: Member
    let emphasize : FamilyViewModel.SortOptions

    var body: some View {
        HStack {
            Text(selectedMember.name)
                .font(emphasize == .name   ? .largeTitle : .body)
            Text("(\(selectedMember.age))")
                .font(emphasize == .age    ? .largeTitle : .body)
            Spacer()
            Text("Klix: \(selectedMember.clickCount)")
                .font(emphasize == .clicks ? .largeTitle : .body)
        }
        .foregroundColor(selectedMember.memberColor)
    }
}

// use the static Member for UI testing.
// Look at this in the preview pane! Cool!
struct MemberView_Previews: PreviewProvider {
    static var previews: some View {
        MemberView(selectedMember: Member.example, emphasize: .clicks)
            .previewLayout(.sizeThatFits)
        MemberView(selectedMember: Member.example, emphasize: .name)
            .previewLayout(.sizeThatFits)
        MemberView(selectedMember: Member.example, emphasize: .age)
            .previewLayout(.sizeThatFits)
    }
}

Family ViewModel

Remember the intentions you though about? They are coded here. One array of family members serves as the source of truth. It is private. No other view can see it or modify it. If it needs to be modified, you've just identified another intention! The other array of family members is the one published to views in your application.

//  Created by Obelix on 2022.02.14.
//  Love coding on Valentine's Day!
//
import SwiftUI
// Link between a Family model and the View showing members in a Family
// Your view wants to show members in a family.
// This is the Model for that view. It holds an array of family members.
class FamilyViewModel : ObservableObject {
    @Published var viewFamily = [Member]()  // external. push to views
    private var familyMembers = [Member]()  // internal. source of truth

    init() { loadFamily() } // classes need initializers

    // Assign strings to your options to use in the Picker view. Awesome!
    enum SortOptions: String, Equatable, CaseIterable {
        case name    = "Name"
        case clicks  = "Clicks"
        case age     = "Age"
        var localizedName: LocalizedStringKey { LocalizedStringKey(rawValue)}
    }

    // Intentions ----------------------------
    // These are your business rules!
    // Keep these OUT of the views.
    func sortFamily(by sortOption: FamilyViewModel.SortOptions) {
        switch sortOption {
        case .clicks: familyMembers.sort { $0.clickCount > $1.clickCount }
        case    .age: familyMembers.sort { $0.age        < $1.age        }
        default:      familyMembers.sort { $0.name       < $1.name       }
        }
        viewFamily = familyMembers // replace it.
    }

    // Populate the View's model for testing.
    func loadFamily() {
        familyMembers.append( Member(name: "David",     age: 52, clickCount: 12 ))
        familyMembers.append( Member(name: "Anna",      age: 50, clickCount: 10 ))
        familyMembers.append( Member(name: "Nicholas",  age: 20, clickCount:  5 ))
        familyMembers.append( Member(name: "Alexandra", age: 18 ))
        familyMembers.append( Member(name: "Obelix",    age: 15 ))
        sortFamily(by: .name) // default starting sort order
    }
}

Family View

This is the display case. Keep this free of business rules. Just display. Pass the business to the view model to process.

//  Created by Obelix on 2022.02.14.
//
import SwiftUI
struct FamilyView: View {
    @StateObject var vm = FamilyViewModel() // wire up a new view model
    @State private var sortSelection = FamilyViewModel.SortOptions.name // default
    var body: some View {
        NavigationView {
            VStack { // Decide how the list is sorted ------------
                Picker("", selection: $sortSelection) {
                    ForEach( FamilyViewModel.SortOptions.allCases, id: \.self ) { option in
                        Text(option.localizedName).tag(option)
                    }
                }
                .onChange(of: sortSelection, perform: { sortOption in
                    // What do you intend to happen then the selection changes?
                    // Tell the view model what your intention is!
                    vm.sortFamily(by: sortOption)
                })
                .pickerStyle(SegmentedPickerStyle())
                .padding(.horizontal)
                // -------------------- List ------------------
                List (vm.viewFamily) { member in
                    MemberView(selectedMember: member, emphasize: sortSelection)
                        .onTapGesture {
                            // What do you intend to happen when the user taps a family member?
                            // Tell the objects your intentions!
                            member.updateClick() // let model handle the details
                            vm.sortFamily(by: sortSelection)
                        }
                }
            }.navigationTitle("Family List")
        }.navigationViewStyle(.stack)
    }
}

struct FamilyView_Previews: PreviewProvider { static var previews: some View { FamilyView()  }

Whew! Happy Valentine's Day.

Love and Kisses,

Obelix

1      

Thank you Obelix.

1      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.