Skip to content

Create SwiftUI Views able to access host windows from UIKit (iOS & tvOS) or AppKit (macOS), with zero set up. Works for existing apps, apps with @main/App, and even Playgrounds

License

Notifications You must be signed in to change notification settings

happycodelucky/SwiftUIWindowBinder

Repository files navigation

SwiftUIWindowBinder

Swift Version iOS Version macOS Version tvOS Version watchOS Version GitHub license Maintained Release

Overview

SwiftUIWindowBinder supports getting SwiftUI access to a host Window object (UIWindow or NSWindow) with zero set up. SwiftUI apps without an application delegate or scene delegate can still access the Window, and the window is scoped to each document in a multi-window application. Playgrounds are also supported here.

Installation

To use SwiftUIWindowBinder within your project see how to reference package using the Swift Package Manager or in Xcode, using this repository's GitHub link. Once installed you can import SwiftUIWindowBinder as appropriate.

Adding to Package.swift

To add manually in the Package.swift use the following reference in your dependencies section:

dependencies: [
    .package(
        name: "SwiftUIWindowBinder".
        url: "https://github.com/happycodelucky/SwiftUIWindowBinder.git", 
        .upToNextMajor(from: "1.0.0"),
],

...

Usage & Examples

Documentation here on the README.md is light. There's not a whole lot to the package, you are encouraged to explore the code and offer fixes/comments on better ways, or additions you might like.

This package does come with ample documentation (I hope), through a set of Swift Playgrounds pages in the package itself. Walk through the documentation, run the code, and definitely read up about the Gotchas!

The playgrounds examples are still in draft. They are all runnable, just watch out for some typos.

In good conscience I can't have no code on a README, so look out below.

Playgrounds

To run the SwiftIWindowBinder Playground examples you will need to open the package in Xcode and run any of the playgrounds under Playgrounds.

Be sure to have the options 'Render Documentation' and 'Build Active Schema' enabled (they are by default) for the best representation, as the Playgrounds serve as working documentation.

Although the package supports iOS 13, macOS 10.15, and tvOS 13, you will need to use Xcode 12.2 to run the playgrounds. If you are on Catalina (macOS 10.15) then the 'macOS' target for the playground will not run due to requiring Big Sur (macOS 11) to run some of the SwiftUI code.

Examples

There are only two real examples of demonstrate here. Using something called WindowBinder and a convenience called WindowButton. As the playground Wrapping Up documentation eludes to, there could be more done here (such as supporting event view modifiers like onTapGesture), but was chosen to avoid. If you want to know more, read through the Playgrounds ;).

WindowBinder

WindowBinder is at the core of capturing a Window in your SwiftUI View. As it name implies it uses a Binding parameter to bind a Window to a @State property of the view (Window is a platform abstraction type alias for UIWindow or NSWindow).

A WindowBinder is a view injected into the actual view hierarchy in UIKit or AppKit, able to tap into the hosted UIWindow or NSWindow respectively.

The following show the use of WindowBinder , binding to self.$window, followed by the trailing closure for the content views. The closure contains a Text view with the onTapGesture view modifier using the bound window property.

import SwiftUI
import SwiftUIWindowBinder

struct ContentView : View {
    /// You will need a `@State` property in your view for the binding
    @State var window: Window?

    /// View body
    var body: some View {
        // Create a WindowBinder and bind it to the state property `window`
        WindowBinder(window: $window) {
          
            Text("Hello")
                .padding()
                .onTapGesture {
                    // `self.window` will be nil initially, until (this) View's actual view is in the
                    // hosted window hierarchy
                    guard let window = window else {
                        return
                    }

                    // Print the window description
                    print(window.description)
                }
          
        }
    }
}

There is no requirement your view be authored in this way. WindowBinder is not required to be a root view, or even contain any of your view element, it just needs to be in your view. Below is an acceptable alternative. The content closure is a convenience to avoid the need for a stack.

struct ContentView : View {
    @State var window: Window?

    var body: some View {
        ZStack(alignment: .center, content: {
            WindowBinder(window: $window) { /* Nothing */ }
          
            Text("Hello")
                .padding()
                .onTapGesture { /* ... */}
        }
    }
}

WindowButton

Buttons are probably where window related actions may be used most. For convenience WindowButton wraps the logic of WindowBinder and provides the action: closure to with a platform dependent Window when interacted with.

Modifying the example above we get a much simpler looking ContentView:

import SwiftUI
import SwiftUIWindowBinder

struct ContentView : View {
    /// View body
    var body: some View {
      
        // Our button that receives a `Window` when interacted with
        WindowButton { window in
            // Print the window description
            print(window.description)
        } label: {
            Text("Hello")
        }
        // Just like Button, WindowButton can be styled just the same
        .buttonStyle(DefaultButtonStyle())
      
    }
}

Unlike the WindowBinder example there is no guard for window. This is because in the case there is no host window the button action: closure will not be called. Given the architecture of SwiftUI/UIKit/AppKit you would need to be doing something out of the ordinary to interact with a view that's not in a host window's view hierarchy.

Wait, There WindowButton But No Event View Modifiers?

Correct! For good reasons. Checkout the Wrapping Up for those reasons and if you really, really want it, a code example.

Enjoy!

SwiftUI is evolving, getting a ton of new features each release. This package represents a bit of a polyfill assistance (hello JavaScript + Babel nomenclature) until the time when we have official wrappers for the things we want.

I don't see UIKit or AppKit disappearing from beneath SwiftUI anytime soon (likely never), and a few of us will continue to find a need for such things like this package.

Of course, bugs, issues, pull requests, corrections, suggestions, or comments fire away...

About

Create SwiftUI Views able to access host windows from UIKit (iOS & tvOS) or AppKit (macOS), with zero set up. Works for existing apps, apps with @main/App, and even Playgrounds

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages