Search code examples
iosswiftswiftuiuiviewcontrollerrepresentable

Conditionally assigning a view of type UIViewControllerRepresentable fails to render changes when State is changed


Pressing the "FIRST" or "SECOND" Text views should change the State selection and update selectedView accordingly. While the debugger shows that pressing "FIRST" or "SECOND" causes body to be re-evaluated and correctly assigns selectedView, the screen never updates to show the correct view. Instead, the screen only shows the view that corresponds to selection's initialized state and remains stuck on that view.

import UIKit
import SwiftUI

struct TestView: View {
    
    @State private var selection: Int = 1
    
    var body: some View {
        let firstView = TestRepresentable(string: "First View")
        let secondView = TestRepresentable(string: "Second View")
        let selectedView = selection == 1 ? firstView : secondView

        let finalView = VStack {
            HStack {
                Text("FIRST").onTapGesture { self.selection = 1 }.padding()
                Text("SECOND").onTapGesture { self.selection = 2 }.padding()
            }
            selectedView
        }
        
        return finalView
    }
}

struct TestRepresentable: UIViewControllerRepresentable {

    let string: String

    func makeUIViewController(context: Context) -> TestVC {
        return TestVC().create(string)
    }

    func updateUIViewController(_ uiViewController: TestVC, context: Context) {
        //...
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(testRepresentable: self)
    }

    class Coordinator: NSObject {

        var testRepresentable: TestRepresentable

        init(testRepresentable: TestRepresentable) {
            self.testRepresentable = testRepresentable
        }
    }
}

class TestVC: UIViewController {
    
    private let label = UILabel()
    
    override func loadView() {
        super.loadView()
        view.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        label.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        label.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
    }
    
    func create(_ string: String) -> TestVC {
        label.text = string
        return self
    }
}

When I swap TestRepresentable(string: "First View") with Text("First View") (and do the same for "Second View"), the screen updates correctly. Something about my UIViewController or UIViewControllerRepresentable implementations prevent changes in my SwiftUI view from rendering on the screen. Why don't changes to the State variable selection render the correct selectedView?


Solution

  • It is just not a SwiftUI coding (so body could not correctly track state changes), here is a fixed part of code (tested with Xcode 13 / iOS 15)

    demo

    var body: some View {
        VStack {
            HStack {
                Text("FIRST").onTapGesture { self.selection = 1 }.padding()
                Text("SECOND").onTapGesture { self.selection = 2 }.padding()
            }
            if selection == 1 {
                TestRepresentable(string: "First View")
            } else {
                TestRepresentable(string: "Second View")
            }
        }
    }