Данный документ актуален по состоянию на стабильный XCode 11.1
На iPhone из
NavigationView
осуществляется push переход черезNavigationLink
в destination. После возврата pop, видим что память, которую занимал detination, не освободилась.
Дело в том что по умолчанию NavigationView
использует DoubleColumnNavigationViewStyle
, который под капотом превращается в SplitViewController
. Он то и удерживает сильную ссылку на destination.
Нужно явно указать StackNavigationViewStyle
для iPhone. В случае если на iPad нужен SplitViewController
, то нужно указать это условием при описании структуры View
NavigationView {
NavigationLink(destination: Text("destination"), label: {
Text("push")
})
}
.navigationViewStyle(StackNavigationViewStyle())
class ViewModel: ObservableObject { }
struct RootView: View {
var body: some View {
VStack {
Text("Parent")
ContentView()
}
}
}
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
Text("Content")
}
}
При обновлении RootView
его body
пересоздается, создавая новое описание ContentView
с новым экземпляром ViewModel
. Чтобы этого не происходило - нужно где-то хранить ссылку на ViewModel
.
Хранить ее можно в UIHostingController
.
struct ContentView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
Text("Content")
}
}
let vc = UIHostingController(rootView: ContentView(viewModel: ViewModel()))
Далее нужно как-то дать возможность этот контроллер описывать в struct
view.
public protocol ViewModelProtocol: class {
static func instanceInView() -> UIViewController
var bindings: Set<AnyCancellable> { get set }
func onAppear()
func onDisappear()
}
extension ViewModelProtocol {
func bind(uiViewController: UIViewController) {
uiViewController.publisher(for: \.parent)
.sink(receiveValue: { [weak self] (parent) in
if parent == nil {
self?.bindings.cancel()
}
})
.store(in: &bindings)
}
}
struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
return ViewModel.instanceInView()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
//
}
}
class ViewModel: ObservableObject {
static func instanceInView() -> UIViewController {
let vm = ViewModel()
let vc = UIHostingController(rootView: ContentView(viewModel: ViewModel))
vm.bind(uiViewController: vc)
return vc
}
}
struct RootView: View {
var body: some View {
ModelView<ContentViewModel>()
}
}
Как удерживается ссылка:
graph LR
A[parent: UIViewController?] -- strong --> B[UIHostingController]
B -- strong --> C[ContentViewModel]
B -- weak --> A
A -- cancel bindings on nil --> C
Отмену подписок можно и не делать, но в таком случае нужно чтобы все sink
в ViewModel захватывали слабую ссылку на ViewModel, чтобы не писать лишний код и не думать о retain cycle проще отписать просто все, правда?)
struct ContentView: View {
@State var isPresented: Bool = false
var body: some View {
VStack {
Button(action: {
self.isPresented = true
}, label: {
Text("Present")
})
Text("Content")
.sheet(isPresented: $isPresented, content: {
Text("Will never be shown")
})
}
.sheet(isPresented: $isPresented, content: {
Text("Sheet")
})
}
}
В данном примере sheet
компонента Text
никогда не будет показан, так как VStack
определил sheet
уровнем выше.
При встраивании в Section
of Form
ломается поведение navigationTitle
. Text
указанный при создании PickerView
не уходит в navigationTitle
.
Нажатие на элементы списка, который создался автоматически при описании PickerView
происходит только непосредственно в том месте где находится текст, а не по всей ширине сроки.
При создании PickerView
отдельно его Text
постоянно присутствует на экране и отнимает место у крутилки.
Не работает инициализатор, который принимает formatter
, текст просто не форматируется и не изменяется.
Даже если написать свою логику форматирования - после каждого форматирования будет смещаться курсор.