0

Hello I had spend over 10 hour to implement Combine framework but can't clear understanding how to link Publisher and Subscriber. In example I just wanna call setTheme funk from Theme class and automatically update game variable in Game class. I know how to achieve it with didSet but main goal to make it with Combine. Would be thankfull for help.

import SwiftUI
import Combine
import Foundation

class Theme: ObservableObject {
    
    @Published private(set) var choosenTheme: Color? // Publisher right?
    
    func setTheme(with color: Color?) {
        if let unwrappedColor = color {
            self.choosenTheme = unwrappedColor
        } else {
            self.choosenTheme = nil
        }
    }
}

class Game: ObservableObject {
    
    @Published var game: String? // Subscriber right?
    private let gameTheme = Theme()
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        addSubscribers()
    }
    
    private func addSubscribers() { // I think something wrong here
        gameTheme.$choosenTheme
            .map(createGame)
            .sink { [weak self] (returnedString) in
                print("Value from sink \(String(describing: returnedString))")
                self?.game = returnedString
            }
            .store(in: &cancellables)
    }
    
    private func createGame(for theme: Color?) -> String? {
       if let unwrappedTheme = theme {
           print(unwrappedTheme)
           return String("\(unwrappedTheme)")
       } else {
           return nil
       }
   }
}

// Test:
var testTheme = Theme()
testTheme.setTheme(with: .orange)

var testGame = Game()
print(testGame.game) // Should be Orange

testTheme.setTheme(with: .blue)
print(testGame.game) // Should be Blue

testTheme.setTheme(with: nil)
print(testGame.game) // Should be nil
Nizami
  • 728
  • 1
  • 6
  • 24
  • That's not what Combine is for. Just make Theme a struct and SwiftUI will detect changes automatically. – malhal Jun 15 '22 at 12:34
  • @malhal as you can see Theme conforms to ObservableObject, I need this behavior and I tried to make example easy to reproduce without additional logic – Nizami Jun 15 '22 at 12:55
  • Usually there is only one object in SwiftUI that holds the model structs in @Published properties – malhal Jun 15 '22 at 16:55
  • @malhal In case if I have different types of games and allow user to add own themes should I put everything in one ViewModel with a list of publishers? Would be thankful if you may be share some articles or project examples on git – Nizami Jun 16 '22 at 04:43
  • 1
    Yes but the object that holds the model structs is usually called a store and not a view model. I recommend apple's wwdc videos to learn SwiftUI cause 3rd party articles usually have mistakes – malhal Jun 16 '22 at 14:13

2 Answers2

0

It shouldn't change at all. What you are doin is setting the theme of an instance of Theme. However, in your Game class you're listening to another one.

Do you see the problem now? either initialize your Game with the theme you're updating or make Theme a singleton.

Try this:

import SwiftUI
import Combine
import Foundation

class Theme: ObservableObject {
    static var shared = Theme()
    @Published private(set) var choosenTheme: Color? // Publisher right?
    
    func setTheme(with color: Color?) {
        if let unwrappedColor = color {
            self.choosenTheme = unwrappedColor
        } else {
            self.choosenTheme = nil
        }
    }
}

class Game: ObservableObject {
    
    @Published var game: String? // Subscriber right?
    private let gameTheme = Theme.shared
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        addSubscribers()
    }
    
    private func addSubscribers() { // I think something wrong here
        gameTheme.$choosenTheme
            .map(createGame)
            .sink { [weak self] (returnedString) in
                print("Value from sink \(String(describing: returnedString))")
                self?.game = returnedString
            }
            .store(in: &cancellables)
    }
    
    private func createGame(for theme: Color?) -> String? {
       if let unwrappedTheme = theme {
           print(unwrappedTheme)
           return String("\(unwrappedTheme)")
       } else {
           return nil
       }
   }
}

// Test:

var testGame = Game() //Game initialization should precede Theme because now the listener starts listening for the orange change 

var testTheme = Theme.shared
testTheme.setTheme(with: .orange)

print(testGame.game) // Should be Orange

testTheme.setTheme(with: .blue)
print(testGame.game) // Should be Blue

testTheme.setTheme(with: nil)
print(testGame.game) // Should be nil
Timmy
  • 4,098
  • 2
  • 14
  • 34
0

The problem you are having is that there are two instances of Theme

one that you create here:

// Test:
var testTheme = Theme()

and another one inside Game:

private let gameTheme = Theme()

Then you are changing the first theme and expecting the second one to change. To fix the problem you have to inject the Theme into your Game.

First you need to make some changes to Game to allow of the Theme to be injected:

class Game: ObservableObject {
    
    @Published var game: String?
    private let gameTheme: Theme
    private var cancellables = Set<AnyCancellable>()
    
    init(gameTheme: Theme) {
        self.gameTheme = gameTheme
        addSubscribers()
    }

Next you have to perform that injection when you test:

var testTheme = Theme()
testTheme.setTheme(with: .orange)

var testGame = Game(gameTheme: testTheme)

If you do that - you will see the changes correctly reflected in prints.

You could simplify your two classes significantly if you wished:

class Theme: ObservableObject {
    
    @Published private(set) var chosenTheme: Color?
    
    func setTheme(with color: Color?) {
        chosenTheme = color
    }
}

class Game: ObservableObject {
    
    @Published var game: String?
    private let gameTheme: Theme
    
    init(gameTheme: Theme) {
        self.gameTheme = gameTheme
        addBindings()
    }
    
    private func addBindings() {
        gameTheme
            .$chosenTheme
            .map(createGame)
            .assign(to: &$game)
    }
    
    private func createGame(for theme: Color?) -> String? {
        theme.map { $0.description }
   }
}
LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57