Данный документ актуален по состоянию на стабильный 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, текст просто не форматируется и не изменяется.
Даже если написать свою логику форматирования - после каждого форматирования будет смещаться курсор.