我的目标是遵守依赖倒置原则。这意味着 SheetView 应该依赖于 Sheet ViewModel 的协议。
问题是当我将 ViewModel 传递给工作表视图的构造函数时,当我关闭工作表视图时,它不会取消初始化。
.sheet(isPresented: $viewModel.isSheetPresented) {
let params = SheetViewModelParams(
initialCount: 0,
initialViewType: .List
)
/**
- Bug: ViewModel will not deinit. Use .init(params:)
*/
// let viewModel = SheetViewModel(params: params)
// SheetView(viewModel: viewModel)
SheetView(params: params)
}
完整代码:swift-bloc-example
下面的代码测试:
=== 此测试所需的代码。
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 body: some View {
VStack(spacing: 8) {
Text("Content Without State")
switch viewModel.onSubmitStatus {
case .initial:
Button {
Task {
await viewModel.onSubmit()
}
} label: {
Text("Submit")
}
.onAppear {
print("\(type(of: self)) initial")
}
case .loading:
ProgressView()
.onAppear {
print("\(type(of: self)) loading")
}
case .success:
Text("Success")
.onAppear {
print("\(type(of: self)) Success")
}
case .failure:
Text("Failure")
.onAppear {
print("\(type(of: self)) Failure")
}
}
CountComponent(
count: $viewModel.count,
onDecrement: {
viewModel.count -= 1
},
onIncrement: {
viewModel.count += 1
}
)
Button {
viewModel.isSheetPresented = true
} label: {
Text("show the sheet")
}
}
.padding(.all, 16)
.border(.secondary)
.onReceive(viewModel.objectWillChange, perform: { _ in
renderCount += 1
print("\(type(of: self)) viewModel will change. count: \(renderCount)")
})
.sheet(isPresented: $viewModel.isSheetPresented) {
let params = SheetViewModelParams(
initialCount: 0,
initialViewType: .List
)
/**
- Bug: ViewModel will not 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 initial
case loading
case success
case failure
}
ContentWithoutStateViewModelParams
struct ContentWithoutStateViewModelParams {
let initialCount: Int
let initialOnSubmitStatus: OnSubmitStatus
let initialIsSheetPresented: Bool
init(
initialCount: Int = 0,
initialOnSubmitStatus: OnSubmitStatus = .initial,
initialIsSheetPresented: Bool = false
) {
self.initialCount = initialCount
self.initialOnSubmitStatus = initialOnSubmitStatus
self.initialIsSheetPresented = initialIsSheetPresented
}
}
ContentWithoutStateViewModel.swift
final class ContentWithoutStateViewModel: ObservableObject {
@Published var count: Int
@Published var onSubmitStatus: OnSubmitStatus
@Published var isSheetPresented: Bool
init(params: ContentWithoutStateViewModelParams) {
self.count = params.initialCount
self.onSubmitStatus = params.initialOnSubmitStatus
self.isSheetPresented = params.initialIsSheetPresented
print("\(type(of: self)) \(#function)")
}
deinit {
print("\(type(of: self)) \(#function)")
}
func fetchContent() async -> Result<Bool, Error> {
sleep(1)
return .success(true)
}
@MainActor
func onSubmit() async {
onSubmitStatus = .loading
let result = await fetchContent()
result.fold { success in
count += 1
onSubmitStatus = .success
} errorTransform: { failure in
count -= 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?
}
==== 片材
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)
)
}
@available(
*,
deprecated,
message: "Bug: ViewModel will not deinit when Sheet is dismissed. use .init(params:)")
init(viewModel: SheetViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
VStack(spacing: 8) {
Text("Sheet")
CountComponent(
count: $viewModel.count,
onDecrement: {
viewModel.count -= 1
},
onIncrement: {
viewModel.count += 1
}
)
Button {
viewModel.selectedViewType = .List
} label: {
Text("show the sheet list")
}
Button {
viewModel.selectedViewType = .New
} label: {
Text("show the sheet new")
}
switch viewModel.selectedViewType {
case .List:
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, perform: { _ in
renderCount += 1
print("\(type(of: self)) viewModel will change. count: \(renderCount)")
})
}
}
#Preview {
let params = SheetViewModelParams(
initialCount: 0,
initialViewType: .List
)
return SheetView(params: params)
}
SheetViewType.swift
enum SheetViewType {
case List
case New
}
SheetViewModelParams.swift
struct SheetViewModelParams {
let initialCount: Int
let initialViewType: SheetViewType
}
SheetViewModel.swift
import Foundation
final class SheetViewModel: ObservableObject {
let id: UUID = UUID()
@Published var count: Int
@Published var selectedViewType: SheetViewType
init(
params: SheetViewModelParams
) {
self.count = params.initialCount
self.selectedViewType = params.initialViewType
print("\(type(of: self)) \(#function) \(id)")
}
deinit {
print("\(type(of: self)) \(#function) \(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!
}
private static weak var _shared: SheetViewModel?
}
=== 图纸列表
struct SheetListView: View {
@State var renderCount: Int = 0
@StateObject var viewModel: SheetListViewModel
init(params: SheetListViewModelParams) {
self._viewModel = StateObject(
wrappedValue: SheetListViewModel.shared(params: params)
)
}
var body: some View {
VStack(spacing: 8) {
Text("Sheet List")
CountComponent(
count: $viewModel.count,
onDecrement: {
viewModel.count -= 1
},
onIncrement: {
viewModel.count += 1
}
)
}
.padding(.all, 16)
.border(.secondary)
.onReceive(viewModel.objectWillChange, perform: { _ in
renderCount += 1
print("\(type(of: self)) viewModel will change. count: \(renderCount)")
})
}
}
#Preview {
let params = SheetListViewModelParams(initialCount: 0)
return SheetListView(params: params)
}
SheetListViewModelParams.swift
import Foundation
struct SheetListViewModelParams {
let initialCount: Int
}
SheetListViewModel.swift
import Foundation
final class SheetListViewModel: ObservableObject {
let id = UUID()
@Published var count: Int
init(params: SheetListViewModelParams) {
self.count = params.initialCount
print("\(type(of: self)) \(#function) \(id)")
}
deinit {
print("\(type(of: self)) \(#function) \(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!
}
private static weak var _shared: SheetListViewModel?
}
=== 新表
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 body: some View {
VStack(spacing: 8) {
Text("Sheet New")
CountComponent(
count: $viewModel.count,
onDecrement: {
viewModel.count -= 1
},
onIncrement: {
viewModel.count += 1
}
)
}
.padding(.all, 16)
.border(.secondary)
.onReceive(viewModel.objectWillChange, perform: { _ in
renderCount += 1
print("\(type(of: self)) viewModel will change. count: \(renderCount)")
})
}
}
#Preview {
let params = SheetNewViewModelParams(initialCount: 0)
return SheetNewView(params: params)
}
SheetNewViewModelParams.swift
struct SheetNewViewModelParams {
let initialCount: Int
}
SheetNewViewModel.swift
import Foundation
final class SheetNewViewModel: ObservableObject {
let id: UUID = UUID()
@Published var count: Int
init(params: SheetNewViewModelParams) {
self.count = params.initialCount
print("\(type(of: self)) \(#function) \(id)")
}
deinit {
print("\(type(of: self)) \(#function) \(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!
}
private static weak var _shared: SheetNewViewModel!
}
这似乎是苹果端的一个合法错误。将 viewModel 传递给 View 的构造函数并不会真正“导致”内存泄漏,但它允许内存泄漏发生。否则你所做的应该没问题:
这似乎是 iOS 17 上的一个已知问题(r.115856582),影响工作表和全屏演示文稿