UIViewControllerRepresentable is crashing on Xcode 14 iOS 16 - ios

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) {
}
}

Related

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.

UIViewControllerRepresentable causes memory leak when in NavigationView

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

How to get notified in SwiftUI when UIKit UIViewController viewDidLoad finished

I have a UIViewController working fine in SwiftUI with the below code.
import Foundation
import SwiftUI
final class RTCVideoViewController: UIViewController {
var previewView: RTCEAGLVideoView!
var videoTrack: RTCVideoTrack!
override func viewDidLoad() {
previewView = RTCEAGLVideoView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
view.addSubview(previewView)
do {
videoTrack = try MediaCapturer.shared.createVideoTrack(videoView: previewView)
} catch {
print(error)
}
}
}
extension RTCVideoViewController : UIViewControllerRepresentable{
public typealias UIViewControllerType = RTCVideoViewController
public func makeUIViewController(context: UIViewControllerRepresentableContext<RTCVideoViewController>) -> RTCVideoViewController {
return RTCVideoViewController()
}
public func updateUIViewController(_ uiViewController: RTCVideoViewController, context: UIViewControllerRepresentableContext<RTCVideoViewController>) {
}
}
In SwiftUI view.
import Combine
import SwiftUI
struct LiveView: View {
#ObservedObject var viewModel: LiveViewModel
init(viewModel: LiveViewModel) {
self.viewModel = viewModel
}
var body: some View {
return ZStack {
RTCVideoViewController()
.edgesIgnoringSafeArea(.top)
}
}
}
So far everything works fine but I need the RTCVideoTrack when created to pass it to the LiveViewModel.
Any ideas what pattern I could implement to get notified when viewDidLoad finished or RTCVideoTrack passed to the LiveViewModel?
I changed a bit the code, it was close but simplified more.
import Foundation
import SwiftUI
struct RTCVideoViewController : UIViewControllerRepresentable {
var viewModel: LiveViewModel
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {
print("HostBroadcastViewController:updateUIViewController")
let previewView = RTCEAGLVideoView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
viewController.view.addSubview(previewView)
do {
let videoTrack = try MediaCapturer.shared.createVideoTrack(videoView: previewView)
self.viewModel.videoTrack = videoTrack
}
catch {
print("ERROR getting RTCVideoTrack")
}
}
}
And in SwiftUI init the RTCVideoViewController with RTCVideoViewController(viewModel: self.viewModel) where ViewModel passed in view from parent.

How do you access UIViewControllerRepresentable NavigationBar property?

In SwiftUI if you are transitioning using a NavigationLink() into a UIViewControllerRepresentable how would you; say, add buttons or change the title property on the navigationbar.
This is what I am doing right now:
import SwiftUI
/// Controls the actual action performed by the button upon taps.
struct CatagoryButton: View {
#State var isPresenting :Bool = false
var company : Company?
var text : String
var body: some View {
NavigationLink(destination: UIKitWrapper(company: self.company, storyboardPointer: self.text)
.navigationBarTitle(self.text)
.edgesIgnoringSafeArea(.all),
isActive: self.$isPresenting,
label: {
Button(action: {
self.isPresenting.toggle()
}){
ZStack {
ButtonShadowLayer(text: text)
GradientBackground()
.mask(ButtonBaseLayer())
CircleAndTextLayer(text: text)
}
}
})
}
}
Here is the struct for my representable.
import SwiftUI
/// Wraps UIKIT instance in a representable that swiftUI can present.
struct UIKitWrapper: UIViewControllerRepresentable {
//Specify what type of controller is being wrapped in an associated type.
typealias UIViewControllerType = UIViewController
//Company property passed from parent view. Represents the company the user selected from main view.
private var company : Company
//Determines which viewcontroller will be presented to user. This string corresponds to the name of the storyboard file in the main bundle.
private var storyboardPointer : String
init(company: Company?, storyboardPointer: String) {
guard let company = company else {fatalError()}
self.company = company
self.storyboardPointer = storyboardPointer
}
func makeUIViewController(context: Context) -> UIViewControllerType {
//Find user defined storyboard in bundle using name.
let storyboard = UIStoryboard(name: storyboardPointer, bundle: .main)
//Downcast returned controller to protocol AccessControllerProtocol. This step is required because we are not sure which storyboard will be accessed. Potential storyboard controllers that can be called all conform to this protocol.
//FIXME: Remove fatalError and create error enum asap.
guard let viewController = storyboard.instantiateInitialViewController() as? AccessControllerProtocol else { fatalError() }
//Assign user selected company object to instance property on incoming viewController.
viewController.company = company
//Return UINavigationController with storyboard instance view controller as root controller.
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Finally, here is one of the classes that use the representable.
import UIKit
class OrdersViewController: UIViewController, AccessControllerProtocol {
var company : Company!
#IBOutlet var companyNameLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setBackgroundColor()
companyNameLabel.text = company.name
self.navigationController?.navigationItem.rightBarButtonItems = [UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.tapRightBarButton))]
}
func setBackgroundColor(){
let backgroundGradient = BackgroundGradientSetter()
let viewWithGradient = backgroundGradient.setGradientToView(with: [DarkBlueHue_DEFAULT,LightBlueHue_DEFAULT], size: view.bounds)
view.addSubview(viewWithGradient)
view.sendSubviewToBack(viewWithGradient)
}
#objc func tapRightBarButton(){
}
}
No matter what I do this button doesn't show up. I'm not sure if I need to put this in a makeCoordinator() or if there is just something I am missing. If anyone has insight please let me know!
If it isn't available in viewDidLoad, try calling your setupNavigation() in viewWillAppear()
In your case navigationController is not available yet on viewDidLoad, try instead as in below demo module
Tested & works with Xcode 11.2 / iOS 13.2
class MyUIController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.navigationController?.navigationBar.topItem?.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.onAdd(_:)))
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// might be needed to remove injected item here
}
#objc func onAdd(_ sender: Any?) {
print(">> tapped add")
}
}
struct MyInjector: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<MyInjector>) -> MyUIController {
MyUIController()
}
func updateUIViewController(_ uiViewController: MyUIController, context: UIViewControllerRepresentableContext<MyInjector>) {
}
}
struct DemoNavigationBarUIButton: View {
var body: some View {
NavigationView {
MyInjector()
.navigationBarTitle("Demo")
}
}
}
struct DemoNavigationBarUIButton_Previews: PreviewProvider {
static var previews: some View {
DemoNavigationBarUIButton()
}
}

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.

Resources