How to handle Views that display and modify elements from an array in Swift/SwiftUI/MVVM

Pretty basic question, I have professional dev experience but brand new to Swift and still figuring out its design patterns and such. I’ve been working through the Stanford course. The first couple assignments surround making a card game, and I’m trying to make the architecture make sense.

The basic unit is a Card struct

struct Card: Identifiable, Equatable {
    let id: String
    var isFaceUp = false
    // other stuff not relevant here...
}

The Model class holds an array of Cards, there’s a very simple viewmodel in the middle that just has a computed property var cards = model.cards, and then the content view lays all the cards out in a grid

  var body: some View {
        LazyVGrid(...) {
            ForEach(viewModel.cards) { card in
                CardView(card: card, viewModel: viewModel)
            }
        }

On tap, a card should flip over.

struct CardView: View {
    let card: Card
    @ObservedObject var viewModel: ViewModel
    var body: some View {
        //draw a card        
        .onTapGesture { viewModel.choose(card: card) }
    }
}

The problem is how to actually flip that card in the model. The way given in the Stanford lectures is to pass the card (by value!) to the viewModel, which passes it to the model. Mutating this card doesn’t do anything since it’s a copy of the “real” card in the array, so we then linear search through the array to find the real card and mutate that, something like

for index in cards.indices {
    if cards[index] == chosenCard {
        cards[index].isFaceUp.toggle()
        return
    }
}

This works, but doesn’t seem right – that’s an O(n) operation on every button press! Not something that strictly needs optimizing but the architecture smells a bit to me and I’m not sure what the alternative is.

Things that come to mind:

  1. Make Card a class, so each CardView can hold a pointer and manipulate it directly. Would that work? And would it violate good practice to directly hold pointers to model objects?
  2. Make the view only hold an index into the model’s cards array, skipping the search step. Problem: the array order changes when the deck is shuffled
  3. Ok then make the view hold the unique card id, the make cards an OrderedDictionary so the deck can have a defined order but constant-time access to a given card
  4. Or let the view hold the Card struct but make cards an OrderedSet?
  5. Something interesting with observables?
  6. ???

This seems super simple but I’m having trouble wrapping my head around how Swift and SwiftUI manage data. It seems pretty built around ‘functional functions’ with immutable inputs rather than methods with side effects, but that makes it hard to actually maintain a source of truth.

What is the properly Swifty way to design this?

New contributor

Caketray is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

1

One thing you can do is declare the card variable a binding, like this:

@Binding var card: Card

The Binding property wrapper is used when the source of truth for the variable needs to be outside the View (as in your situation, where the source of truth is held in the model). So when you create the view, you pass in the card like this:

ForEach($model.cards) { $card in // the $ indicates a binding
    CardView(card)
}

Using a binding this way means that the model is listening for changes made to the card in CardView and will update the card when it’s updated in the view.

This is a pretty good explanation of @State and @Binding property wrappers: https://www.kodeco.com/books/swiftui-cookbook/v1.0/chapters/1-understanding-state-binding-in-swiftui

2

Regarding your self-answers: don’t overcomplicate the matter.

Instead, keep these simple rules in mind:

  1. A view should be simply a function of state.

So, what does that mean? Well, the view renders what ever state (or better view state) it has been provided – here, from your View Model. The view state is just a composition of trivial data that precisely represents every aspect of your view.

So, in your case, “precisely” means, that the view state contains information whether a card is flipped or not. Remember, the view just renders exactly what the view state is: view = f(view_state).

  1. A View should not mutate its state

This is a consequence form #1, just emphasising it. 😉

  1. A View sends Commands to the View Model

A command is any user intent, for example a button tap, or something that happens in the view, for example “on appear”.

One can model all commands in this system with an enum, for example:

enum Event {
    case onAppear
    case cardTapped(id: CardID)
}

Now, the View Model is the computational model which computes a new view state based on the current view state and an event.
The computational model can be expressed with a function:

(State, Event) -> State

This may implement your presentation logic, or if you want also business logic.

This computational model can be easily extended to call any service functions:

(State, Event) -> (State, [ServiceFunction])

Where ServiceFunction is a struct which contains a function which will be called in your ViewModel implementation. This represents the “Model” in the MVVM pattern.

Note, that it is a very simple, yet powerful approach. It can be used to implement any MV** pattern and also ELM, Redux, TCA, and potentially more.

So, in order to implement an MVVM pattern, first identify the “view state”. Note, it exactly models your view, using only trivial data types. You can end up with either an enum (modelling different “modes” of your view), or a struct.

Then identify the events that can happen in this use case. One can model it with an enum. Probably the easiest part.

Regarding implementing the View Model. Well, it might become an Observable – this a class, which should also be confined to the main thread, i.e. make it a @MainActor. Basically it would suffice to implement the function mentioned above. Actually it will only be a few lines of code, and it will usually be very generic (actually, it can be a generic class, with State and Event as type parameter).

It’s also possible to implement the function and the associated state only utilising SwiftUI. So, the “View Model” will be implemented with a SwiftUI view.

The above pattern will more or less enforce you to get an event driven, unidirectional approach. It deliberately does not allow to use two-way bindings, where a view actually can mutate its state, which would violate rule #1.

I don’t want to suggest, how to implement a view model. But, in my opinionated preferred way, my View Model is a generic class, with type parameters State and Event. It uses a state machine, and pretty much directly implements the above function, which is a combination of the transition function and an output function from a Finite State Machine. This View Model can also handle and manage “tasks”, i.e. it can call function which creates Tasks and can also cancel them. These tasks usually have “side effects” – i.e. calling a backend. These tasks may also emit one or several events which then get fed back into the system. The whole machinery can also be implemented within SwiftUI view – so no Observable, just SwiftUI.

If we would find a protocol for a view model, it would just be:

protocol ViewModel {
    associatedtype State
    associatedtype Event

    var state { get }
    func send(_ event: Event)
}

For a conforming view model, the state is observable. The generic initialisers receive the transition function and the initial state.

Hope this helps 😉

2

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật