How to model data using Combine & SwiftUI - swift

I've been learning Swift & SwiftUI and all has been going well until very recently. I have successfully used #Published properties to keep my data & views in sync. However, I now want to display some data that is a combination of several #Published properties.
The simplified data model class:
import Foundation
final class GameAPI: ObservableObject {
struct PlayerStats: Identifiable, Codable {
var gamesPlayed: Int
var wins: Int
var losses: Int
}
struct Player: Identifiable, Codable {
var id = UUID()
var name: String
var stats: PlayerStats
}
struct Room: Identifiable, Codable {
var id = UUID()
var name: String
var players: [Player]
}
struct ServerStats {
var totalGamesPlayed: Int
var totalPlayers: Int
var totalPlayersOnline: Int
}
#Published var players: [Player] = []
#Published var rooms: [Room] = []
func addPlayer(name: String) {
players.append(Player(
name: name,
stats: PlayerStats(
gamesPlayed: 0,
wins: 0,
losses: 0
)
))
}
func removePlayer(id: UUID) { ... }
func updatePlayerStats(playerId: UUID, stats: PlayerStats) { ... }
func getLeaderboard() -> [Player] {
return players.sorted({ $0.stats.wins > $1.stats.wins }).prefix(10)
}
func getServerStats() -> ServerStats {
return ServerStats(
totalGamesPlayed: ...,
totalPlayers: ...,
totalPlayersOnline: ...,
)
}
}
View:
import SwiftUI
struct LeaderboardTabView: View {
#EnvironmentObject var gameAPI: GameAPI
var body: some View {
VStack {
Text("TOP PLAYERS")
Leaderboard(model: gameAPI.getLeaderboard())
// ^^^ How can I make the view automatically refresh when players are added/removed or any of the player stats change?
}
}
}
How can I wire up my views to leaderboard & server stats data so that the view refreshes whenever the data model changes?

Related

swiftUI: updating view

I can't figure it out why view is not updating, please help. In real project I get data via websocket (and set variable with DispatchQueue.main.async {}). Here's the code as an example. After clicking on button nothing happens with the view. I use ObservableObject, Published attributes. What's the problem?
ps. It requires to add some more text to the post, because it's mostly the code, but I don't know what to add, everything is below :)
import SwiftUI
class DataBase: ObservableObject {
#Published var data: [MyData]
#Published var users: [User]
init(data: [MyData], users: [User]) {
self.data = data
self.users = users
}
}
class MyData: ObservableObject, Identifiable {
#Published var type: String
#Published var array: [Double]
init(type: String, array: [Double]) {
self.type = type
self.array = array
}
}
class User: ObservableObject, Identifiable {
#Published var id: UUID = UUID()
#Published var name: String
#Published var data: MyData
init(name: String, data: MyData) {
self.name = name
self.data = data
}
}
let data: [MyData] = [
MyData(type: "type1", array: [1, 2, 3]),
MyData(type: "type2", array: [4, 5, 6, 7]),
]
let users: [User] = [
User(name: "Tim", data: data[0]),
User(name: "Steve", data: data[1]),
]
struct ContentView: View {
let db = DataBase(data: data, users: users)
var body: some View {
ShowView(db: db)
}
}
struct ShowView: View {
#ObservedObject var db: DataBase
var body: some View {
HStack {
List(db.users) { user in
Text("\(user.name) \(user.data.type)")
Text("\(user.data.array.count)")
Divider()
}
List(db.data) { data in
Text("\(data.type)")
Text("\(data.array.count)")
Divider()
}
}
HStack {
Button("add data to data[0]") {
db.data[0].array.append(db.data[0].array.last! + 10)
print(db.data[0].array)
}
Button("add data to data[1]") {
db.data[1].array.append(db.data[1].array.last! + 20)
print(db.data[1].array)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
try this using objectWillChange, works for me:
struct ShowView: View {
#ObservedObject var db: DataBase
var body: some View {
HStack {
List(db.users) { user in
Text("\(user.name) \(user.data.type)")
Text("\(user.data.array.count)")
Divider()
}
List(db.data) { data in
Text("\(data.type)")
Text("\(data.array.count)")
Divider()
}
}
HStack {
Button("add data to data[0]") {
db.objectWillChange.send() // <-- here
db.data[0].array.append(db.data[0].array.last! + 10)
print(db.data[0].array)
}
Button("add data to data[1]") {
db.objectWillChange.send() // <-- here
db.data[1].array.append(db.data[1].array.last! + 20)
print(db.data[1].array)
}
}
}
}
Just make model as value type (i.e. struct instead of class) - no more changes needed:
struct MyData: Identifiable {
var id = UUID()
var type: String
var array: [Double]
init(type: String, array: [Double]) {
self.type = type
self.array = array
}
}
struct User: Identifiable {
var id: UUID = UUID()
var name: String
var data: MyData
init(name: String, data: MyData) {
self.name = name
self.data = data
}
}
Tested with Xcode 13.4 / iOS 15.5
Update
Then it is needed to create separated views with ObservedObject for every observable model object, like
List(db.users) {
UserRowView(user: $0)
}
struct UserRowView: View {
#ObservedObject var user: User // a class, so needed to be observed
var body: some View {
Text("\(user.name) \(user.data.type)")
Text("\(user.data.array.count)")
Divider()
}
}
the same for MyData, or make a dependency update, like
class User: ObservableObject {
#Published var data: MyData
// ...
private var cancellable: AnyCancellable?
init(...) {
// ....
cancellable = data.objectWillChange.sink { [weak self] _ in
guard let self = self else { return }
self.objectWillChange.send()
}
}
}

Binding ObservedObject Init to ContentView

I'm trying to pull more content from server when the user comes to end of the page. I start with loading 5 posts and then 5+5=10 posts. But I cannot bind range so that the view updates with new numbers. Is there a better approach to this or what am I doing wrong here? I tried many things from similiar questions but here is the code:
struct ContentView: View {
#ObservedObject var feedPosts : PostArrayObject
#State var rangeEnd: Int
init(){
let state = State(initialValue: 1)
self._rangeEnd = state
self.feedPosts = PostArrayObject(personalFeed: true, chunkSize: state.projectedValue)
}
var body: some View {
TabView {
NavigationView {
FeedView(posts: feedPosts, title: "Feed", rangeEnd: $rangeEnd)
.toolbar {
....
class PostArrayObject: ObservableObject {
#Published var range: Range<Int> = 0..<1
init(personalFeed: Bool, chunkSize: Binding<Int>) {
DataService.instance.downloadPostsForFollowingInChunks(chunkSize: chunkSize.wrappedValue) { (returnedPosts) in
let shuffledPosts = returnedPosts.shuffled()
self.dataArray.append(contentsOf: shuffledPosts)
self.postsLoaded = true
}
}
struct FeedView: View {
#ObservedObject var posts: PostArrayObject
var title: String
var chunkSize: Int = 5
#Binding var rangeEnd: Int
....
func loadMore(){
rangeEnd = chunkSize
posts.range = 0..<posts.range.upperBound + rangeEnd
}
}

SwiftUI: reading data from ObservableObject

I am new to SwiftUI and created a Todo model. I then have a class that represents the container so that I can have an array of Todos and methods on it later on. I made TodoContainer an ObservableObject and then observing it after instantiating it: #ObservedObject var items = TodoContainer().
I am iterating through the list and expecting anytime I click the button, shouldn't it appear in the list since it's creating a new Todo each time and allTodos should have the array of all Todos?
import SwiftUI
struct Todo: Identifiable {
var id = UUID()
var name: String;
var priority: Int;
}
class TodoContainer: ObservableObject {
#Published var allTodos = [Todo]();
}
struct ContentView: View {
#ObservedObject var items = TodoContainer()
var body: some View {
Button("Create New") {
Todo(name: "New one", priority: Int.random(in: 1..<100))
}
List(items.allTodos, id: \.id){item in
Text(item.name)
}
}
}
You need to add the created todo to the view model's allTodos. Xcode may have shown you an error about an unused expression previously.
import SwiftUI
struct Todo: Identifiable {
var id = UUID()
var name: String;
var priority: Int;
}
class TodoContainer: ObservableObject {
#Published var allTodos = [Todo]();
}
struct ContentView: View {
#ObservedObject var items = TodoContainer()
var body: some View {
Button("Create New") {
let todo = Todo(name: "New one", priority: Int.random(in: 1..<100))
items.allTodos.append(todo) // here
}
List(items.allTodos, id: \.id){item in
Text(item.name)
}
}
}

SwiftUI MVVM simple concept confusion - advice needed

I'm struggling with some basic MVVM concepts in SwiftUI. I appreciate this is probably a simple question but my brain is frazzled I can't figure it out.
Here's my models/views/viewmodels etc.
import Foundation
struct Challenges {
var all: [Challenge]
init() {
all = []
}
}
struct Challenge: Identifiable, Codable, Hashable {
private(set) var id = UUID()
private(set) var name: String
private(set) var description: String
private(set) var gpxFile: String
private(set) var travelledDistanceMetres: Double = 0
init(name: String, description: String, gpxFile: String) {
self.name = name
self.description = description
self.gpxFile = gpxFile
}
mutating func addDistance(_ distance: Double) {
travelledDistanceMetres += distance
}
}
import SwiftUI
#main
struct ActivityChallengesApp: App {
var body: some Scene {
WindowGroup {
ChallengesView()
.environmentObject(ChallengesViewModel())
}
}
}
import SwiftUI
class ChallengesViewModel: ObservableObject {
#Published var challenges: Challenges
init() {
challenges = Challenges()
challenges.all = DefaultChallenges.ALL
}
func addDistance(_ distance: Double, to challenge: Challenge) {
challenges.all[challenge].addDistance(distance)
}
}
import SwiftUI
struct ChallengesView: View {
#EnvironmentObject var challengesViewModel: ChallengesViewModel
var body: some View {
NavigationView {
List {
ForEach(challengesViewModel.challenges.all) { challenge in
NavigationLink {
ChallengeView(challenge)
.environmentObject(challengesViewModel)
} label: {
VStack(alignment: .leading) {
Text(challenge.name)
Text("\(challenge.travelledDistanceMetres)")
}
}
}
}
.navigationTitle("Challenges")
}
}
}
import SwiftUI
struct ChallengeView: View {
var challenge: Challenge
#EnvironmentObject var challengesViewModel: ChallengesViewModel
init(_ challenge: Challenge) {
self.challenge = challenge
}
var body: some View {
VStack(alignment: .leading) {
Text(challenge.name)
Text("\(challenge.travelledDistanceMetres)")
}
.onTapGesture {
handleTap()
}
}
func handleTap() {
challengesViewModel.addDistance(40, to: challenge)
}
}
I understand the concepts but I'm confused as to what the ViewModel should be.
I feel like this is overkill, i.e. sending a model object to the view and the view model as an environment object. With this set up, I call the addDistance() function in the view model from within the view to make changes to the model.
ChallengeView(challenge)
.environmentObject(challengesViewModel)
Is it better to have a view model for the collection or one view model per model object?
This is the simplest version I could come up with.
I don't really understand the need for the challenges.all ? So I took it out.
I have
a struct for the single challenge
an observable class which is publishing the challenges array
instantiate this once with #StateObject and pass it down as you did
btw: You don't need explicit initializers for structs
this is it:
#main
struct ActivityChallengesApp: App {
// here you create your model once
#StateObject var challenges = ChallengesModel()
var body: some Scene {
WindowGroup {
ChallengesView()
.environmentObject(challenges)
}
}
}
struct Challenge: Identifiable, Codable, Hashable {
var id = UUID()
var name: String
var description: String
var gpxFile: String
var travelledDistanceMetres: Double = 0
mutating func addDistance(_ distance: Double) {
travelledDistanceMetres += distance
}
}
class ChallengesModel: ObservableObject {
#Published var challenges: [Challenge]
init() {
// Test data
challenges = [
Challenge(name: "Challenge One", description: "?", gpxFile: ""),
Challenge(name: "Challenge Two", description: "?", gpxFile: ""),
Challenge(name: "Last Challenge", description: "?", gpxFile: "")
]
}
func addDistance(_ distance: Double, to challenge: Challenge) {
// find the challenge and update it
if let i = challenges.firstIndex(where: {$0.id == challenge.id}) {
challenges[i].addDistance(distance)
}
}
}
struct ChallengesView: View {
#EnvironmentObject var challengesModel: ChallengesModel
var body: some View {
NavigationView {
List {
ForEach(challengesModel.challenges) { challenge in
NavigationLink {
ChallengeView(challenge: challenge)
.environmentObject(challengesModel)
} label: {
VStack(alignment: .leading) {
Text(challenge.name)
Text("\(challenge.travelledDistanceMetres)")
}
}
}
}
.navigationTitle("Challenges")
}
}
}
struct ChallengeView: View {
var challenge: Challenge
#EnvironmentObject var challengesModel: ChallengesModel
var body: some View {
VStack(alignment: .leading) {
Text(challenge.name)
Text("\(challenge.travelledDistanceMetres)")
}
.onTapGesture {
handleTap()
}
}
func handleTap() {
challengesModel.addDistance(40, to: challenge)
}
}

Declare array property with #published var inside same ObservableObject

I'm trying to declare Array with a #Published var property. However, the value is never updated (Always the default -> 0).
How can I do that ? What am I doing wrong ?
Thank you very much for your help ;)
Have a nice day
struct MyCategory: Identifiable, Hashable {
let id = UUID()
let name: String
let amount: Double
}
class MyViewModel: ObservableObject {
#Published var amount1: Double = 0
#Published var amount2: Double = 0
#Published var categories = [MyCategory]()
init() {
self.categories = [
MyCategory(name: "Test 1", amount: self.amount1),
MyCategory(name: "Test 2", amount: self.amount2)
]
}
func updateAmounts(value1: Double, value2: Double) {
self.amount1 += value1
self.amount2 += value2
}
}
import SwiftUI
struct MyView: View {
#EnvironmentObject var myViewModel: MyViewModel
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
ForEach(myViewModel.categories, id: \.self) { category in
HStack() {
Text("\(category.name)")
Text("\(category.amount)") // Value not refresh! 0
}
}
}
}
}