I write SwiftUI module, but there is a feature that uses UIKit functionality. So in my SwiftUI view there is a UIViewRepresentable wrapper, that makes UIKit controller. In its turn it makes a collection view where view is SwiftUI view.
So I have a chain like this: View -> ViewRepresentable -> CollectionView -> View
And the problem is that I have #State variable in the first View, and I want to access and change it in the last view. How can I do it?
FirstView.swift
...
#ObservedObject var viewModel: ViewModel
struct FirstView: View {
var body: some View {
CollectionWrapper()
.environmentObject(viewModel)
}
}
...
Wrapper.swift
...
struct CollectionWrapper: UIViewRepresentable {
#EnvironmentObject var viewModel: Model
func makeUIView(context: Context) -> CollectionView {
CollectionView() // how I can pass the variable? DI is not actually working right.
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
...
CollectionView.swift
...
class CollectionView: UICollectionView {
...
func makeView() -> UIView {
// Make hosting controller etc.
hostingContoller.view = ModelView() // and here I somehow should pass my view model
}
...
}
...
I have already tried to do DI through the wrapper in makeUIView method, but my app is crashing because .environmentObject doesn't work in hosting controller for some reason.
Navigation bar buttons are not tappable after dismissing a sheet in SwiftUI. Below is the steps to reproduce the issue
Present a sheet,
Move the app to background for a short duration (2 seconds)
Resume the app & dismiss the sheet by swiping down
Now the navigation bar button frames are misaligned. Tap is working at different frame than visible frame of the button. This is easily reproducible on iOS 16 simulator, but intermittently on actual iOS devices. Below is the minimal code to reproduce the issue
struct ContentView: View {
#State private var showSheetView = false
var body: some View {
NavigationView {
VStack {
navigationBarView
Color.blue
}
.sheet(isPresented: $showSheetView) {
FilterView()
}
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
}
private var navigationBarView: some View {
HStack(spacing: 0) {
Spacer()
Button {
showSheetView = true
} label: {
Text("Filter")
.padding()
.background(Color.red)
}
}
}
}
struct FilterView: View {
var body: some View {
Color.green
}
}
I've also struggled a lot with this issue, and it's clearly a bug from apple.
I found an ugly hack to workaround this issue. But to make it work I have to redraw my main view every time I enter background. For my project it's ok until they fix an actual bug. I hope it will help you as well
When you detect app is moving to background, you force app to redraw based on UUID()
struct BackgroundModeSheetBugApp: App {
#State private var id = UUID()
var body: some Scene {
WindowGroup {
ContentView()
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
id = UUID()
}
.id(id)
}
}
}
I've tested it with your code and it's working correctly on both simulator and device
P.S. I also took courage to use your example to submit a bug to Apple
P.P.S. For my app I've also had to wrap my main view to hack UIViewControllerRepresentable to fix the padding issues.
struct UIHackMainTabView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIHostingController<MainTabView> {
let mainTab = MainTabView()
return UIHostingController(rootView: mainTab)
}
func updateUIViewController(_ pageViewController: UIHostingController<MainTabView>, context: Context) {
}
}
Another workaround for now is to make use of presentationDetents()
It's break UI a little, since we're loosing nice shrink effect and it works only on iOS 16, but at least app will work there :(
struct SheetHackModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.presentationDetents([.fraction(0.99)])
} else {
content
}
}
}
extension View {
func sheetHackModifier() -> some View {
self.modifier(SheetHackModifier())
}
}
I am working on app supporting iOS 13 using SwiftUI, I have custom activity indicator(UIViewRepresentable) on which is displayed on Api call
In main screen where I have created NavigationView, on top of that I am able to add the activity indicator view which is able to cover the navigation bar also,
But in detail view which is navigated using navigation link, I tried the same code but the activity indicator view is displayed below navigation bar(which is not covering the navigation bar)
struct DetailView: View {
#State private var show = false
var body: some View {
ZStack {
Button("showSpinner") {
self.show.toggle()
}
}
.modifier(CoverNavigationBar(show: $show) {
CustomActiviyIndicatorView()
.edgesIgnoringSafeArea(.all)
})
} }
struct CoverNavigationBar<Cover: View>: ViewModifier {
#Binding var show: Bool
let cover: () -> Cover
func body(content: Content) -> some View {
ZStack {
content
if self.show {
cover()
}
}
} }
I have a swiftUI view that calls a UIViewRepresentable view. In the SwiftUI view I am toggling the state of #State boolean value.
In my UIViewRepresentable view I have created a binding that gets past from the main SwiftUI view. The problem is the binding never gets update or at least the updateView function is not getting called in the UIViewRepresentable view. I fell like I must be doing something wrong but I am just overlooking it. Here is an example of what I am trying to do.
import Foundation
import SwiftUI
struct BindingTest: UIViewRepresentable {
#Binding var status: Bool
func makeUIView(context: Context) -> UIActivityIndicatorView {
let activityIndicator = UIActivityIndicatorView()
activityIndicator.style = .large
return activityIndicator
}
func updateUIView(_ uiView: UIViewType, context: Context) {
print("Hello")
}
}
import SwiftUI
struct ChartView: View {
#State var status = false
var body: some View {
VStack{
Spacer()
Button(action: {
status = !status
}) {
Text("Change")
}
BindingTest(status: $status)
}
}
}
I am using Xcode 12.5 and Swift 5.4
Looks like SwiftUI is doing some cleverness under the hood that isn't immediately obvious to us. Because you don't actually use your binding in updateUIView, it's not actually getting called.
However, if you update your code to the following:
func updateUIView(_ uiView: UIViewType, context: Context) {
print("Hello \(status)")
}
then you'll see that it does, in fact, get called.
PS - you can use status.toggle() instead of status = !status
I'm trying to find a way to trigger an action that will call a function in my UIView when a button gets tapped inside swiftUI.
Here's my setup:
foo()(UIView) needs to run when Button(SwiftUI) gets tapped
My custom UIView class making use of AVFoundation frameworks
class SomeView: UIView {
func foo() {}
}
To use my UIView inside swiftUI I have to wrap it in UIViewRepresentable
struct SomeViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CaptureView {
SomeView()
}
func updateUIView(_ uiView: CaptureView, context: Context) {
}
}
SwiftUI View that hosts my UIView()
struct ContentView : View {
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable()
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
}) {
Text("Tap Here")
}
}
}
}
}
You can store an instance of your custom UIView in your representable struct (SomeViewRepresentable here) and call its methods on tap actions:
struct SomeViewRepresentable: UIViewRepresentable {
let someView = SomeView() // add this instance
func makeUIView(context: Context) -> SomeView { // changed your CaptureView to SomeView to make it compile
someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
func callFoo() {
someView.foo()
}
}
And your view body will look like this:
let someView = SomeViewRepresentable()
var body: some View {
VStack(alignment: .center, spacing: 24) {
someView
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
self.someView.callFoo()
}) {
Text("Tap Here")
}
}
}
}
To test it I added a print to the foo() method:
class SomeView: UIView {
func foo() {
print("foo called!")
}
}
Now tapping on your button will trigger foo() and the print statement will be shown.
M Reza's solution works for simple situations, however if your parent SwiftUI view has state changes, every time when it refreshes, it will cause your UIViewRepresentable to create new instance of UIView because of this: let someView = SomeView() // add this instance. Therefore someView.foo() is calling the action on the previous instance of SomeView you created, which is already outdated upon refreshing, so you might not see any updates of your UIViewRepresentable appear on your parent view.
See: https://medium.com/zendesk-engineering/swiftui-uiview-a-simple-mistake-b794bd8c5678
A better practice would be to avoid creating and referencing that instance of UIView when calling its function.
My adaption to M Reza's solution would be calling the function indirectly through parent view's state change, which triggers updateUIView :
var body: some View {
#State var buttonPressed: Bool = false
VStack(alignment: .center, spacing: 24) {
//pass in the #State variable which triggers actions in updateUIVIew
SomeViewRepresentable(buttonPressed: $buttonPressed)
.background(Color.gray)
HStack {
Button(action: {
buttonPressed = true
}) {
Text("Tap Here")
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#Binding var buttonPressed: Bool
func makeUIView(context: Context) -> SomeView {
return SomeView()
}
//called every time buttonPressed is updated
func updateUIView(_ uiView: SomeView, context: Context) {
if buttonPressed {
//called on that instance of SomeView that you see in the parent view
uiView.foo()
buttonPressed = false
}
}
}
Here's another way to do it using a bridging class.
//SwiftUI
struct SomeView: View{
var bridge: BridgeStuff?
var body: some View{
Button("Click Me"){
bridge?.yo()
}
}
}
//UIKit or AppKit (use NS instead of UI)
class BridgeStuff{
var yo:() -> Void = {}
}
class YourViewController: UIViewController{
override func viewDidLoad(){
let bridge = BridgeStuff()
let view = UIHostingController(rootView: SomeView(bridge: bridge))
bridge.yo = { [weak self] in
print("Yo")
self?.howdy()
}
}
func howdy(){
print("Howdy")
}
}
Here is yet another solution! Communicate between the superview and the UIViewRepresentable using a closure:
struct ContentView: View {
/// This closure will be initialized in our subview
#State var closure: (() -> Void)?
var body: some View {
SomeViewRepresentable(closure: $closure)
Button("Tap here!") {
closure?()
}
}
}
Then initialize the closure in the UIViewRepresentable:
struct SomeViewRepresentable: UIViewRepresentable {
// This is the same closure that our superview will call
#Binding var closure: (() -> Void)?
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// Since `closure` is part of our state, we can only set it on the main thread
DispatchQueue.main.async {
closure = {
// Perform some action on our UIView
}
}
return uiView
}
}
#ada10086 has a great answer. Just thought I'd provide an alternative solution that would be more convenient if you want to send many different actions to your UIView.
The key is to use PassthroughSubject from Combine to send messages from the superview to the UIViewRepresentable.
struct ContentView: View {
/// This will act as a messenger to our subview
private var messenger = PassthroughSubject<String, Never>()
var body: some View {
SomeViewRepresentable(messenger: messenger) // Pass the messenger to our subview
Button("Tap here!") {
// Send a message
messenger.send("button-tapped")
}
}
}
Then we monitor the PassthroughSubject in our subview:
struct SomeViewRepresentable: UIViewRepresentable {
let messenger = PassthroughSubject<String, Never>()
#State private var subscriptions: Set<AnyCancellable> = []
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// This must be run on the main thread
DispatchQueue.main.async {
// Subscribe to messages
messenger.sink { message in
switch message {
// Call funcs in `uiView` depending on which message we received
}
}
.store(in: &subscriptions)
}
return uiView
}
}
This approach is nice because you can send any string to the subview, so you can design a whole messaging scheme.
My solution is to create an intermediary SomeViewModel object. The object stores an optional closure, which is assigned an action when SomeView is created.
struct ContentView: View {
// parent view holds the state object
#StateObject var someViewModel = SomeViewModel()
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable(model: someViewModel)
.background(Color.gray)
HStack {
Button {
someViewModel.foo?()
} label: {
Text("Tap Here")
}
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#ObservedObject var model: SomeViewModel
func makeUIView(context: Context) -> SomeView {
let someView = SomeView()
// we don't want the model to hold on to a reference to 'someView', so we capture it with the 'weak' keyword
model.foo = { [weak someView] in
someView?.foo()
}
return someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
}
class SomeViewModel: ObservableObject {
var foo: (() -> Void)? = nil
}
Three benefits doing it this way:
We avoid the original problem that #ada10086 identified with #m-reza's solution; creating the view only within the makeUIView function, as per the guidance from Apple Docs, which state that we "must implement this method and use it to create your view object."
We avoid the problem that #orschaef identified with #ada10086's alternative solution; we're not modifying state during a view update.
By using ObservableObject for the model, we can add #Published properties to the model and communicate state changes from the UIView object. For instance, if SomeView uses KVO for some of its properties, we can create an observer that will update some #Published properties, which will be propagated to any interested SwiftUI views.