Skip to content

Instantly share code, notes, and snippets.

@timothycosta
Last active November 9, 2024 23:09
Show Gist options
  • Save timothycosta/a43dfe25f1d8a37c71341a1ebaf82213 to your computer and use it in GitHub Desktop.
Save timothycosta/a43dfe25f1d8a37c71341a1ebaf82213 to your computer and use it in GitHub Desktop.
Using UIViewController via the SwiftUI Environment
struct ViewControllerHolder {
weak var value: UIViewController?
init(_ value: UIViewController?) {
self.value = value
}
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder { return ViewControllerHolder(UIApplication.shared.windows.first?.rootViewController ) }
}
extension EnvironmentValues {
var viewController: ViewControllerHolder {
get { return self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
extension UIViewController {
func present<Content: View>(presentationStyle: UIModalPresentationStyle = .automatic, transitionStyle: UIModalTransitionStyle = .coverVertical, animated: Bool = true, completion: @escaping () -> Void = {}, @ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = presentationStyle
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(toPresent))
)
if presentationStyle == .overCurrentContext {
toPresent.view.backgroundColor = .clear
}
self.present(toPresent, animated: animated, completion: completion)
}
}
@timothycosta
Copy link
Author

I put this code into a simple project, presented a UIHostingController from window.rootViewController and then did self.viewControllerHolder.value?.dismiss(...). I don't see any leaks in the memory graph after dismissal.

If you didn't directly copy and paste this, ensure that you're declaring extension EnvironmentValues { var viewController: ViewControllerHolder { ... } } and not var viewController: UIViewController? { ... } as the latter definitely will leak.

@bobshoemaker
Copy link

Hi! Thanks so much for the solution, I'm fairly new to swift and I was also running into the memory leak problem but once I had updated the code it seems like there is a problem while building. I ran into a key path value type mismatch error on the @Environment(.viewController) private var viewControllerHolder: UIViewController? line

do you have any idea wha could be the problem? Thanks!

@haikusw
Copy link

haikusw commented Jun 1, 2020

@bobshoemaker wrote:

Hi! Thanks so much for the solution, I'm fairly new to swift and I was also running into the memory leak problem but once I had updated the code it seems like there is a problem while building. I ran into a key path value type mismatch error on the @Environment(.viewController) private var viewControllerHolder: UIViewController? line

do you have any idea wha could be the problem? Thanks!

The .viewController in @Environment(\.viewController) refers to the EnvironmentValues extension's var definition on line 14 of the gist. If you refer to that you will see that it is defined as a ViewControllerHolder, not a UIViewController.
(Also you are missing the keypath starting "" in your comment above, but I assume that is just local to this comment and not your code).

So set the type of the var to ViewControllerHolder and that may make the compiler happy (it's a compiler, so who knows? ;)).
Something like:

@Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder?

@bobshoemaker
Copy link

bobshoemaker commented Jun 4, 2020

Thanks! As you said it's just a compiler issue! I've just changed it to:

@Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder?
private var viewController: UIViewController? {
   self.viewControllerHolder?.value
}

The memory leak is gone as well! Thanks again!

@tarasis
Copy link

tarasis commented Oct 21, 2020

Thanks for this! I have a couple of questions

Is there a way to make the modal only as big as the content you put in it? I don’t want the white box that appears when using .formSheet/pageSheet. (I know I can just set toPresent.view.backgroundColor = .clear) preferredContentSize seems like an option but haven’t had a chance to test

Also when I tried to add a button to the body of ModalContentView to dismiss (experimenting with full screen), the button appeared behind the content, not above it like I expected given its a VStack. I can’t therefore see how to dismiss full screen

@Sadmansamee
Copy link

@Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder?
private var viewController: UIViewController? {
   self.viewControllerHolder?.value
}

with this code I'm getting error like
Key path value type 'ViewControllerHolder' cannot be converted to contextual type 'ViewControllerHolder?

@haikusw
Copy link

haikusw commented Jan 12, 2021

@Sadmansamee

@Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder?
private var viewController: UIViewController? {
   self.viewControllerHolder?.value
}

with this code I'm getting error like

Key path value type 'ViewControllerHolder' cannot be converted to contextual type 'ViewControllerHolder?

Don't have time to test this but maybe try:

@Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder
private var viewController: UIViewController? {
    self.viewControllerHolder.value
}

Reading the gist, Line 14 defines viewController to return a non-optional ViewControllerHolder so I'm guessing that's the issue you're running into.

@pushpankq
Copy link

and how to dismiss it

@iosdroid
Copy link

iosdroid commented Feb 4, 2022

its support for RTL?

@malhal
Copy link

malhal commented Nov 9, 2024

Using UIViewControllerRepresentable to manage the make/update lifetime will fix the leak.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment