I have a Model class containing the main data (@Published var counter: Int), which is automatically updated periodically by a Timer. I am considering displaying the Model.counter value in a SheetView.
I believe a simple case would look like this.
@main
struct BindingSampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@MainActor
class ContentModel: ObservableObject {
@Published var counter: Int = 0
@Published var isShowingSheet = false
init() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
Task { @MainActor in
self.counter += 1
print("counter: \(self.counter)")
}
}
}
func showSheet() {
isShowingSheet = true
}
}
struct ContentView: View {
@StateObject var model = ContentModel()
var body: some View {
VStack {
Button() {
model.showSheet()
} label: {
Text("Show. \(model.counter)")
}
}
.sheet(isPresented: $model.isShowingSheet) {
SheetView(counter: model.counter)
}
.padding()
}
}
struct SheetView: View {
let counter: Int
var body: some View {
VStack {
Text("\(counter)")
.font(.largeTitle)
}
}
}
Due to the increasing complexity of SheetView's logic, I've created a separate SheetModel. What I want to achieve is to display the value of Model.counter, which is updated by a timer, in SheetView by relaying it through SheetModel.
After some trial and error, I have come up with the following two pieces of code that seem to achieve the desired behavior. However, I am not sure if this is the appropriate way to use SwiftUI.
Specifically, for the first code snippet, self.sheetModel = SheetModel(counter: self._counter), I suspect it may not be ideal because it probably causes both Model and SheetModel to reference the same object.
For the second snippet, SheetModel.init(counter: Published<Int>.Publisher) { counter.assign(to: &self.$counter) }, it feels a bit verbose.
Is there a simpler and better way to bind Model.counter and SheetModel.counter?
(Tested on Xcode Version 14.3.1 (14E300c), iOS simulator 16.4)
First
@main
struct BindingSampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@MainActor
class ContentModel: ObservableObject {
@Published var counter: Int = 0
@Published var sheetModel: SheetModel?
var isShowingSheet: Bool {
get {
sheetModel != nil
}
set {
if !newValue {
sheetModel = nil
}
}
}
init() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
Task { @MainActor in
self.counter += 1
print("counter: \(self.counter)")
}
}
}
func showSheet() {
// I suspect it may not be ideal because it probably causes both Model and
// SheetModel to reference the same object.
self.sheetModel = SheetModel(counter: self._counter)
}
}
@MainActor
class SheetModel: ObservableObject {
@Published var counter: Int = 0
init(counter: Published<Int>) {
print("SheetModel.init")
self._counter = counter
}
}
struct ContentView: View {
@StateObject var model = ContentModel()
var body: some View {
VStack {
Button() {
model.showSheet()
} label: {
Text("Show. \(model.counter)")
}
}
.sheet(isPresented: $model.isShowingSheet) {
SheetView(model: model.sheetModel!)
}
.padding()
}
}
struct SheetView: View {
@ObservedObject var model: SheetModel
var body: some View {
VStack {
Text("\(model.counter)")
.font(.largeTitle)
}
}
}
Second
@MainActor
class ContentModel: ObservableObject {
@Published var counter: Int = 0
@Published var sheetModel: SheetModel?
var isShowingSheet: Bool {
get {
sheetModel != nil
}
set {
if !newValue {
sheetModel = nil
}
}
}
init() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
Task { @MainActor in
self.counter += 1
print("counter: \(self.counter)")
}
}
}
func showSheet() {
self.sheetModel = SheetModel(counter: $counter)
}
}
@MainActor
class SheetModel: ObservableObject {
@Published var counter: Int = 0
init(counter: Published<Int>.Publisher) {
print("SheetModel.init")
// it feels a bit verbose.
counter.assign(to: &self.$counter)
}
}
// ContentView and SheetView are same as above.