Skip to content

Instantly share code, notes, and snippets.

@timothycosta
Last active April 28, 2023 07:00
  • Star 19 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
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)
}
}
@krjw
Copy link

krjw commented Oct 17, 2019

Usage from your StackOverflow answer:

struct MyView: View {

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

    var body: some View {
        Button(action: {
           self.viewController?.present(style: .fullScreen) {
              MyView()
           }
        }) {
           Text("Present me!")
        }
    }
}

@haikusw
Copy link

haikusw commented Nov 17, 2019

It doesn't appear that the style parameter is utilized in the body of the present function and so this doesn't actually present using the passed in style (.fullscreen).

If I insert the line:

toPresent.modalPresentationStyle = style

between line 31 & 32, directly above the last line in the present function, self.present(toPresent, animated: true, completion: nil), then I see the expected full screen behavior.

@timothycosta
Copy link
Author

@haikusw good catch. Thanks!

@haikusw
Copy link

haikusw commented Nov 29, 2019

Sure. This solution still seems to leak memory though and I haven’t been able to figure out why or where/how to fix it yet, regrettably. It’s on my mental list to try again at some point but maybe you understand this enough better than I do to see and fix it?

@timothycosta
Copy link
Author

timothycosta commented Nov 29, 2019

@haikusw Thanks again for the reminder! It leaks because EnvironmentValues is only used once when you actually create the view. After that the view maintains a strong reference to the view controller itself.

Updated the gist with my current code.

@haikusw
Copy link

haikusw commented Nov 29, 2019

Thanks. Are you using Xcode 11.3 beta perhaps?

The new code isn't building for me in the latest release Xcode (11.2.1) with newest possible target of iOS 13.2. doesn't know .anyView on line 26.

@ernestoSk13
Copy link

hi, we have tried to use this approach to present iPad full screen modal views but we are getting memory leaks too :/ did you find any solution or alternative?

@haikusw
Copy link

haikusw commented May 5, 2020

I did not look into it further as I couldn't get Timothy's updated code to build and our design changed enough that we didn't need it. Sorry. Please post here if you do find a solution because I'm sure this will come up again (!).

@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?

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