My purpose is to evolve to the Dependency Inversion precept. That means that SheetView ought to depend upon Sheet ViewModel’s protocol.
The issue is after I move ViewModel to a Sheet View’s constructor, after I dismiss the Sheet View, it won’t deinit.
.sheet(isPresented: $viewModel.isSheetPresented) {
let params = SheetViewModelParams(
initialCount: 0,
initialViewType: .Listing
)
/**
- Bug: ViewModel won't deinit. Use .init(params:)
*/
// let viewModel = SheetViewModel(params: params)
// SheetView(viewModel: viewModel)
SheetView(params: params)
}
Full code: swift-bloc-example
The code under take a look at:
- eagerly load the Sheet View, will solely initialize the Sheet ViewModel when the Sheet View is introduced.
- deinit Sheet ViewModel when the Sheet View is dismissed.
- init Sheet Listing ViewModel when the Sheet Listing View is introduced.
- deinit Sheet Listing ViewModel when the Sheet View or when it current Sheet New View.
=== Code that’s obligatory for this take a look at.
ContentWithoutStateView.swift
import SwiftUI
struct ContentWithoutStateView: View {
@State var renderCount: Int = 0
@StateObject var viewModel: ContentWithoutStateViewModel
init(params: ContentWithoutStateViewModelParams) {
self._viewModel = StateObject(
wrappedValue: ContentWithoutStateViewModel.shared(params: params)
)
}
var physique: some View {
VStack(spacing: 8) {
Textual content("Content material With out State")
change viewModel.onSubmitStatus {
case .preliminary:
Button {
Job {
await viewModel.onSubmit()
}
} label: {
Textual content("Submit")
}
.onAppear {
print("(kind(of: self)) preliminary")
}
case .loading:
ProgressView()
.onAppear {
print("(kind(of: self)) loading")
}
case .success:
Textual content("Success")
.onAppear {
print("(kind(of: self)) Success")
}
case .failure:
Textual content("Failure")
.onAppear {
print("(kind(of: self)) Failure")
}
}
CountComponent(
depend: $viewModel.depend,
onDecrement: {
viewModel.depend -= 1
},
onIncrement: {
viewModel.depend += 1
}
)
Button {
viewModel.isSheetPresented = true
} label: {
Textual content("present the sheet")
}
}
.padding(.all, 16)
.border(.secondary)
.onReceive(viewModel.objectWillChange, carry out: { _ in
renderCount += 1
print("(kind(of: self)) viewModel will change. depend: (renderCount)")
})
.sheet(isPresented: $viewModel.isSheetPresented) {
let params = SheetViewModelParams(
initialCount: 0,
initialViewType: .Listing
)
/**
- Bug: ViewModel won't deinit. Use .init(params:)
*/
// let viewModel = SheetViewModel(params: params)
// SheetView(viewModel: viewModel)
SheetView(params: params)
}
}
}
#Preview {
let params = ContentWithoutStateViewModelParams()
return ContentWithoutStateView(params: params)
}
OnSubmitStatus.swift
enum OnSubmitStatus {
case preliminary
case loading
case success
case failure
}
ContentWithoutStateViewModelParams
struct ContentWithoutStateViewModelParams {
let initialCount: Int
let initialOnSubmitStatus: OnSubmitStatus
let initialIsSheetPresented: Bool
init(
initialCount: Int = 0,
initialOnSubmitStatus: OnSubmitStatus = .preliminary,
initialIsSheetPresented: Bool = false
) {
self.initialCount = initialCount
self.initialOnSubmitStatus = initialOnSubmitStatus
self.initialIsSheetPresented = initialIsSheetPresented
}
}
ContentWithoutStateViewModel.swift
last class ContentWithoutStateViewModel: ObservableObject {
@Printed var depend: Int
@Printed var onSubmitStatus: OnSubmitStatus
@Printed var isSheetPresented: Bool
init(params: ContentWithoutStateViewModelParams) {
self.depend = params.initialCount
self.onSubmitStatus = params.initialOnSubmitStatus
self.isSheetPresented = params.initialIsSheetPresented
print("(kind(of: self)) (#perform)")
}
deinit {
print("(kind(of: self)) (#perform)")
}
func fetchContent() async -> End result<Bool, Error> {
sleep(1)
return .success(true)
}
@MainActor
func onSubmit() async {
onSubmitStatus = .loading
let end result = await fetchContent()
end result.fold { success in
depend += 1
onSubmitStatus = .success
} errorTransform: { failure in
depend -= 1
onSubmitStatus = .failure
}
}
}
ContentWithoutStateViewModel+Shared.swift
extension ContentWithoutStateViewModel {
static func shared(params: ContentWithoutStateViewModelParams) -> ContentWithoutStateViewModel {
var temp: ContentWithoutStateViewModel
if _shared == nil {
temp = ContentWithoutStateViewModel(params: params)
_shared = temp
}
return _shared!
}
static weak var _shared: ContentWithoutStateViewModel?
}
==== Sheet
SheetView.swift
struct SheetView: View {
@State var renderCount: Int = 0
@StateObject var viewModel: SheetViewModel
init(params: SheetViewModelParams) {
self._viewModel = StateObject(
wrappedValue: SheetViewModel.shared(params: params)
)
}
@out there(
*,
deprecated,
message: "Bug: ViewModel won't deinit when Sheet is dismissed. use .init(params:)")
init(viewModel: SheetViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel)
}
var physique: some View {
VStack(spacing: 8) {
Textual content("Sheet")
CountComponent(
depend: $viewModel.depend,
onDecrement: {
viewModel.depend -= 1
},
onIncrement: {
viewModel.depend += 1
}
)
Button {
viewModel.selectedViewType = .Listing
} label: {
Textual content("present the sheet record")
}
Button {
viewModel.selectedViewType = .New
} label: {
Textual content("present the sheet new")
}
change viewModel.selectedViewType {
case .Listing:
let params = SheetListViewModelParams(initialCount: 0)
SheetListView(params: params)
case .New:
let params = SheetNewViewModelParams(initialCount: 0)
SheetNewView(params: params)
}
}
.padding(.all, 16)
.border(.secondary)
.onReceive(viewModel.objectWillChange, carry out: { _ in
renderCount += 1
print("(kind(of: self)) viewModel will change. depend: (renderCount)")
})
}
}
#Preview {
let params = SheetViewModelParams(
initialCount: 0,
initialViewType: .Listing
)
return SheetView(params: params)
}
SheetViewType.swift
enum SheetViewType {
case Listing
case New
}
SheetViewModelParams.swift
struct SheetViewModelParams {
let initialCount: Int
let initialViewType: SheetViewType
}
SheetViewModel.swift
import Basis
last class SheetViewModel: ObservableObject {
let id: UUID = UUID()
@Printed var depend: Int
@Printed var selectedViewType: SheetViewType
init(
params: SheetViewModelParams
) {
self.depend = params.initialCount
self.selectedViewType = params.initialViewType
print("(kind(of: self)) (#perform) (id)")
}
deinit {
print("(kind(of: self)) (#perform) (id)")
}
}
SheetViewModel.swift
extension SheetViewModel {
static func shared(params: SheetViewModelParams) -> SheetViewModel {
var temp: SheetViewModel
if _shared == nil {
temp = SheetViewModel(params: params)
_shared = temp
}
return _shared!
}
non-public static weak var _shared: SheetViewModel?
}
=== Sheet Listing
struct SheetListView: View {
@State var renderCount: Int = 0
@StateObject var viewModel: SheetListViewModel
init(params: SheetListViewModelParams) {
self._viewModel = StateObject(
wrappedValue: SheetListViewModel.shared(params: params)
)
}
var physique: some View {
VStack(spacing: 8) {
Textual content("Sheet Listing")
CountComponent(
depend: $viewModel.depend,
onDecrement: {
viewModel.depend -= 1
},
onIncrement: {
viewModel.depend += 1
}
)
}
.padding(.all, 16)
.border(.secondary)
.onReceive(viewModel.objectWillChange, carry out: { _ in
renderCount += 1
print("(kind(of: self)) viewModel will change. depend: (renderCount)")
})
}
}
#Preview {
let params = SheetListViewModelParams(initialCount: 0)
return SheetListView(params: params)
}
SheetListViewModelParams.swift
import Basis
struct SheetListViewModelParams {
let initialCount: Int
}
SheetListViewModel.swift
import Basis
last class SheetListViewModel: ObservableObject {
let id = UUID()
@Printed var depend: Int
init(params: SheetListViewModelParams) {
self.depend = params.initialCount
print("(kind(of: self)) (#perform) (id)")
}
deinit {
print("(kind(of: self)) (#perform) (id)")
}
}
SheetListViewModel.swift
extension SheetListViewModel {
static func shared(params: SheetListViewModelParams) -> SheetListViewModel {
var temp: SheetListViewModel
if _shared == nil {
temp = SheetListViewModel(params: params)
_shared = temp
}
return _shared!
}
non-public static weak var _shared: SheetListViewModel?
}
=== Sheet New
SheetNew.swift
import SwiftUI
struct SheetNewView: View {
@State var renderCount = 0
@StateObject var viewModel: SheetNewViewModel
init(params: SheetNewViewModelParams) {
self._viewModel = StateObject(
wrappedValue: SheetNewViewModel.shared(params: params)
)
}
var physique: some View {
VStack(spacing: 8) {
Textual content("Sheet New")
CountComponent(
depend: $viewModel.depend,
onDecrement: {
viewModel.depend -= 1
},
onIncrement: {
viewModel.depend += 1
}
)
}
.padding(.all, 16)
.border(.secondary)
.onReceive(viewModel.objectWillChange, carry out: { _ in
renderCount += 1
print("(kind(of: self)) viewModel will change. depend: (renderCount)")
})
}
}
#Preview {
let params = SheetNewViewModelParams(initialCount: 0)
return SheetNewView(params: params)
}
SheetNewViewModelParams.swift
struct SheetNewViewModelParams {
let initialCount: Int
}
SheetNewViewModel.swift
import Basis
last class SheetNewViewModel: ObservableObject {
let id: UUID = UUID()
@Printed var depend: Int
init(params: SheetNewViewModelParams) {
self.depend = params.initialCount
print("(kind(of: self)) (#perform) (id)")
}
deinit {
print("(kind(of: self)) (#perform) (id)")
}
}
SheetNewViewModel.swift
extension SheetNewViewModel {
static func shared(params: SheetNewViewModelParams) -> SheetNewViewModel {
var temp: SheetNewViewModel
if _shared == nil {
temp = SheetNewViewModel(params: params)
_shared = temp
}
return _shared!
}
non-public static weak var _shared: SheetNewViewModel!
}