UIViewControllerRepresentable causes memory leak when in NavigationView - ios

I'm aware about this issue that UIViewControllerRepresentable could cause a memory leak. Even if it should be fixed with past Xcode releases, I'm facing it only when embedding it in a NavigationView. I'm using Xcode Version 11.7 (11E801a) on a physical iPhone 11 iOS 13.7
Here an example:
Memory leak
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView { // Memory leak
ZStack {
Text("Hello, World!")
ViewControllerContainer() // Memory leak
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ViewControllerContainer: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
No memory leak
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Text("Hello, World!")
ViewControllerContainer() // No memory leak
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ViewControllerContainer: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
Is it a xcode/swiftui related bug or I missing something?
EDIT:
Screenshot of Instruments/Leaks
UPDATE:
The leak is not showing up with iPhone 11 (iOS 13.7) simulator

Related

UIViewControllerRepresentable is crashing on Xcode 14 iOS 16

I am trying to create Xcode previews for my view controller in Xcode 14 beta and iOS 16. When ever I run the code, it just throws some Xcode preview error in the dialog and crashes the preview. I am not using Storyboards, so I am just loading my ViewController2 programmatically.
import Foundation
import UIKit
import SwiftUI
final class ViewController2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.green
}
}
struct ViewController2_Previews: PreviewProvider {
static var previews: some View {
ViewController2()
}
}
extension ViewController2: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController2 {
ViewController2()
}
func updateUIViewController(_ uiViewController: ViewController2, context: Context) {
}
}
Ouch... you use class for representable - that's bad idea (even for final!) - use only(!) structs
Here is fixed variant (tested with Xcode 14b2)
struct ViewController2_Previews: PreviewProvider {
static var previews: some View {
ViewControllerRep()
}
}
struct ViewControllerRep: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController2 {
return ViewController2()
}
func updateUIViewController(_ uiViewController: ViewController2, context: Context) {
}
}

Swiftui: How to add Accessibility Identifier on navigationTitle

How can we add the Accessibility Identifier to NaviagationTitle Text. I know for buttons/text/Image/stack views we can use .accessibility(identifier: “some_identifier”).
struct SomeView: View {
var body: some View {
VStack {
Text("Title Text")
.accessibility(identifier: "title")
}
.navigationTitle("title") //How to add accessibilityIdentifier to Navigation bar title?
//.navigationTitle(Text("title").accessibility(identifier: "title"))
}
}
unable to add the modifier to .navigationBarTitle(Text(“title”), displayMode: .inline). Accessibility Identifiers are required for XCUI automation testing.
I don't think this is possible in SwiftUI using .accessibility(identifier:) - it might be worth submitting feedback to Apple.
However, you can still access the navigation bar by its identifier - just the default identifier is the text:
.navigationTitle("title")
let app = XCUIApplication()
app.launch()
assert(app.navigationBars["title"].exists) // true
Alternatively, you can try to access UINavigationBar using a helper extension (adapted from here):
struct NavigationBarAccessor: UIViewControllerRepresentable {
var callback: (UINavigationBar?) -> Void
private let proxyController = ViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<NavigationBarAccessor>) -> UIViewController {
proxyController.callback = callback
return proxyController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NavigationBarAccessor>) {}
typealias UIViewControllerType = UIViewController
private class ViewController: UIViewController {
var callback: (UINavigationBar?) -> Void = { _ in }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
callback(navigationController?.navigationBar)
}
}
}
Now you can access UINavigationBar from a SwiftUI view:
struct ContentView: View {
var body: some View {
NavigationView {
Text("text")
.navigationTitle("title")
.background(
NavigationBarAccessor {
$0?.accessibilityIdentifier = "id123"
}
)
}
}
}
Note that in the above example you set accessibilityIdentifier to the UINavigationBar itself and not to the title directly.

Hide StatusBar And NavigationBar On MainView But Show It On DetailView In Swift

I've written two SwiftUI Views like below. In here, I want to;
Hide statusbar and navigation bar on mainview
Show StatusBar and NavigationBar on DetailView
But when I implement it like below, somethings go wrong.
FirstView.swift
import SwiftUI
struct FirstView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("Main View")
}
}
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(true)
.statusBar(hidden: true)
}
}
SecondView.swift
import SwiftUI
import UIKit
import WebKit
class SecondViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let myURL = URL(string: "https://www.stackoverflow.com")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest)
}
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
view = webView
}
}
private struct SecondViewHolder: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> SecondViewController {
return SecondViewController()
}
func updateUIViewController(_ uiViewController: SecondViewController, context: Context) {
}
}
struct SecondView: View {
var body: some View {
VStack(spacing: 0) {
HeaderView()
SecondViewHolder()
FooterView()
}
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(false)
.statusBar(hidden: true)
}
}
struct HeaderView: View {
var body: some View {
Text("Header Note")
.foregroundColor(.yellow)
.frame(maxWidth: .infinity, minHeight: 60)
.background(Color.red)
}
}
struct FooterView: View {
var body: some View {
Text("Footer Note")
.foregroundColor(.yellow)
.frame(maxWidth: .infinity, minHeight: 100)
.background(Color.red)
}
}
When I run it on IOS simulator:
Views look so wrong. In second page I expect a NavigationBar and Header. But I couldn't see them. I am so new at Swift. Could you please help me about it?
Try the following
struct FirstView: View {
#State private var noBars = true
var body: some View {
NavigationView {
NavigationLink(destination:
SecondView()
.onAppear { self.noBars = false }
.onDisappear { self.noBars = true }
) {
Text("Main View")
}
}
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(noBars)
.statusBar(hidden: noBars)
}
}

Calling a local UIViewController function from a SwiftUI View

I'm trying to call a local ViewController function from ContentView. The function uses some local variables and cannot be moved outside the ViewController.
class ViewController: UIViewController {
func doSomething() {...}
}
extension ViewController : LinkViewDelegate {...}
located on a different file:
struct ContentView: View {
init() {
viewController = .init(nibName:nil, bundle:nil)
}
var viewController: viewController
var body: some View {
Button(action: {self.viewController.doSomething()}) {
Text("Link Account")
}
}
}
UIViewController cannot be changed to something like UIViewRepresentable because LinkViewDelegate can only extend UIViewController.
So you need to create a simple bool binding in SwiftUI, flip it to true to trigger the function call in the UIKit viewController, and then set it back to false until the next time the swiftUI button is pressed. (As for LinkViewDelegate preventing something like UIViewControllerRepresentable that shouldn't stop you, use a Coordinator to handle the delegate calls.)
struct ContentView: View {
#State var willCallFunc = false
var body: some View {
ViewControllerView(isCallingFunc: $willCallFunc)
Button("buttonTitle") {
self.willCallFunc = true
}
}
}
struct ViewControllerView: UIViewControllerRepresentable {
#Binding var isCallingFunc: Bool
func makeUIViewController(context: Context) -> YourViewController {
makeViewController(context: context) //instantiate vc etc.
}
func updateUIViewController(_ uiViewController: YourViewController, context: Context) {
if isCallingFunc {
uiViewController.doSomething()
isCallingFunc = false
}
}
}
Here is a way that I've come up with which doesn't result in the "Modifying state during view update, this will cause undefined behavior" problem. The trick is to pass a reference of your ViewModel into the ViewController itself and then reset the boolean that calls your function there, not in your UIViewControllerRepresentable.
public class MyViewModel: ObservableObject {
#Published public var doSomething: Bool = false
}
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
var body: some View {
MyView(viewModel: viewModel)
Button("Do Something") {
viewModel.doSomething = true
}
}
}
struct MyView: UIViewControllerRepresentable {
#ObservedObject var viewModel: MyViewModel
func makeUIViewController(context: Context) -> MyViewController {
return MyViewController(viewModel)
}
func updateUIViewController(_ viewController: MyViewController, context: Context) {
if viewModel.doSomething {
viewController.doSomething()
// boolean will be reset in viewController
}
}
}
class MyViewController: UIViewController {
var viewModel: MyViewModel
public init(_ viewModel: MyViewModel) {
self.viewModel = viewModel
}
public func doSomething() {
// do something, then reset the flag
viewModel.doSomething = false
}
}
You could pass the instance of ViewController as a parameter to ContentView:
struct ContentView: View {
var viewController: ViewController // first v lowercase, second one Uppercase
var body: some View {
Button(action: { viewController.doSomething() }) { // Lowercase viewController
Text("Link Account")
}
}
init() {
self.viewController = .init(nibName:nil, bundle:nil) // Lowercase viewController
}
}
// Use it for the UIHostingController in SceneDelegate.swift
window.rootViewController = UIHostingController(rootView: ContentView()) // Uppercase ContentView
Updated answer to better fit the question.

in SwiftUI NavigationView Problem with UIViewRepresentable

this is my ContentView.swift file:
struct ContentView: View {
var body: some View {
NavigationView {
MapView()
}
}
}
and this is my MapView.swift file:
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView(frame: .zero)
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
print("test")
}
}
with these lines of codes, when I run app it prints twice test in console.
but when I change ContentView and hide NavigationView from it like this:
struct ContentView: View {
var body: some View {
// NavigationView {
MapView()
// }
}
}
now it prints one test in console.
actually it mean when we use NavigationView with UIViewRepresentable, it calls twice updateUIView func and it seem it is wrong.
how can we handle it?

Resources