Skip to content

Commit

Permalink
Dynamically update window content
Browse files Browse the repository at this point in the history
  • Loading branch information
divadretlaw committed Mar 11, 2024
1 parent b05b8db commit 7f1c1c3
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 28 deletions.
78 changes: 78 additions & 0 deletions Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "635AD8BD2ADABEF200613384"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "635AD8BD2ADABEF200613384"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "635AD8BD2ADABEF200613384"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ Handle `UIWindow` & `UIWindowScene` within SwifUI and present SwiftUI Views in t

## Usage

> [!IMPORTANT]
> Keep in mind that the content is detached from the host view, as it lives in its own context.
> This means in order to pass data between the host view and the content in the window, you need to use
> a model to communicate. For example by using an`ObservableObject`.
### `windowCover(isPresented:content:)`

![Static Badge](https://img.shields.io/badge/Platform_Compability-iOS%20%7C%20visionOS%20%7C%20tvOS-orange?logo=swift&labelColor=white)
Expand Down
2 changes: 0 additions & 2 deletions Sources/WindowKit/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,3 @@ public extension View {
)
}
}

// MARK: - Internal
34 changes: 34 additions & 0 deletions Sources/WindowKit/WindowCover/WindowCover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,34 @@ struct WindowCover<WindowContent>: ViewModifier where WindowContent: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.timeZone) private var timeZone

@WindowIdentifier private var identifier

func body(content: Content) -> some View {
if let key {
content
#if os(visionOS)
.onChange(of: identifier) {
windowManager.update(key: key)
}
.onChange(of: isPresented) { _, value in
if value {
present(with: key)
} else {
dismiss(with: key)
}
}
#else
.onChange(of: identifier) { _ in
windowManager.update(key: key)
}
.onChange(of: isPresented) { value in
if value {
present(with: key)
} else {
dismiss(with: key)
}
}
#endif
.onAppear {
guard isPresented else { return }
present(with: key)
Expand All @@ -49,18 +67,34 @@ struct WindowCover<WindowContent>: ViewModifier where WindowContent: View {
guard value == key else { return }
isPresented = false
}
#if os(visionOS)
.onChange(of: colorScheme) { _, colorScheme in
environmentResolver.colorScheme = colorScheme
}
.onChange(of: timeZone) { _, timeZone in
environmentResolver.timeZone = timeZone
}
#else
.onChange(of: colorScheme) { colorScheme in
environmentResolver.colorScheme = colorScheme
}
.onChange(of: timeZone) { timeZone in
environmentResolver.timeZone = timeZone
}
#endif
} else {
content
#if os(visionOS)
.onChange(of: isPresented) { _, value in
guard value else { return }
Logger.main.error("[Presentation] Attempt to present a window cover without a window scene.")
}
#else
.onChange(of: isPresented) { value in
guard value else { return }
Logger.main.error("[Presentation] Attempt to present a window cover without a window scene.")
}
#endif
.onAppear {
guard isPresented else { return }
Logger.main.error("[Presentation] Attempt to present a window cover without a window scene.")
Expand Down
13 changes: 9 additions & 4 deletions Sources/WindowKit/WindowCover/WindowCoverHostingController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@

import SwiftUI

@MainActor
final class WindowCoverHostingController<Content>: UIHostingController<Content> where Content: View {
final class WindowCoverHostingController<Content>: UIHostingController<Content>, DynamicProperty where Content: View {
var key: WindowKey
var builder: () -> Content

init(key: WindowKey, rootView: Content) {
init(key: WindowKey, builder: @escaping () -> Content) {
self.key = key
super.init(rootView: rootView)
self.builder = builder
super.init(rootView: builder())
}

func update() {
rootView = builder()
}

@available(*, unavailable)
Expand Down
24 changes: 24 additions & 0 deletions Sources/WindowKit/WindowIdentifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// WindowIdentifier.swift
// WindowKit
//
// Created by David Walter on 11.03.24.
//

import SwiftUI

@propertyWrapper struct WindowIdentifier: DynamicProperty, Hashable {
var wrappedValue: UUID

init() {
self.wrappedValue = UUID()
}

mutating func update() {
wrappedValue = UUID()
}

func hash(into hasher: inout Hasher) {
hasher.combine(wrappedValue)
}
}
32 changes: 20 additions & 12 deletions Sources/WindowKit/WindowManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,10 @@ final class WindowManager: ObservableObject {
self.dismissSubject = PassthroughSubject()
}

private func makeIterator() -> [WindowKey: UIWindow].Iterator {
allWindows.makeIterator()
}

func presentCover<Content>(
key: WindowKey,
with configuration: WindowCoverConfiguration,
view: (UIWindow) -> Content
view: @escaping (UIWindow) -> Content
) where Content: View {
guard allWindows[key] == nil else {
// swiftlint:disable:next line_length
Expand All @@ -48,9 +44,9 @@ final class WindowManager: ObservableObject {
window.overrideUserInterfaceStyle = configuration.userInterfaceStyle
window.tintColor = configuration.tintColor

let rootView = view(window)

let hostingController = WindowCoverHostingController(key: key, rootView: rootView)
let hostingController = WindowCoverHostingController(key: key) {
view(window)
}
hostingController.modalTransitionStyle = configuration.modalTransitionStyle
hostingController.modalPresentationStyle = configuration.modalPresentationStyle
hostingController.overrideUserInterfaceStyle = configuration.userInterfaceStyle
Expand All @@ -67,7 +63,7 @@ final class WindowManager: ObservableObject {
func presentOverlay<Content>(
key: WindowKey,
with configuration: WindowOverlayConfiguration,
view: (UIWindow) -> Content
view: @escaping (UIWindow) -> Content
) where Content: View {
guard allWindows[key] == nil else {
// swiftlint:disable:next line_length
Expand All @@ -78,9 +74,9 @@ final class WindowManager: ObservableObject {
let window = PassthroughWindow(windowScene: key.windowScene)
allWindows[key] = window

let rootView = view(window)

let viewController = WindowOverlayHostingController(key: key, rootView: rootView)
let viewController = WindowOverlayHostingController(key: key) {
view(window)
}

viewController.overrideUserInterfaceStyle = configuration.userInterfaceStyle

Expand All @@ -93,6 +89,18 @@ final class WindowManager: ObservableObject {
window.isHidden = false
}

func update(key: WindowKey) {
guard let window = allWindows[key] else {
return
}

guard var viewController = window.rootViewController as? DynamicProperty else {
return
}

viewController.update()
}

func dismiss(with key: WindowKey) {
guard let window = allWindows[key] else {
return
Expand Down
17 changes: 15 additions & 2 deletions Sources/WindowKit/WindowOverlay/WindowOverlay.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// WindowCover.swift
// WindowOverlay.swift
// WindowKit
//
// Created by David Walter on 15.10.23.
Expand All @@ -8,7 +8,7 @@
import SwiftUI
import OSLog

struct WindowOverlay<WindowContent>: ViewModifier where WindowContent: View {
struct WindowOverlay<WindowContent>: ViewModifier, DynamicProperty where WindowContent: View {
@Environment(\.windowLevel) private var windowLevel

@State var key: WindowKey?
Expand All @@ -18,6 +18,10 @@ struct WindowOverlay<WindowContent>: ViewModifier where WindowContent: View {
@ObservedObject private var windowManager = WindowManager.shared
@EnvironmentInjectedObject private var environmentHolder: EnvironmentValuesHolder

@State private var hostingController: WindowOverlayHostingController<AnyView>?

@WindowIdentifier private var identifier

func body(content: Content) -> some View {
if let key {
content
Expand All @@ -27,6 +31,15 @@ struct WindowOverlay<WindowContent>: ViewModifier where WindowContent: View {
.onDisappear {
dismiss(with: key)
}
#if os(visionOS)
.onChange(of: identifier) {
windowManager.update(key: key)
}
#else
.onChange(of: identifier) { _ in
windowManager.update(key: key)
}
#endif
} else {
content
.onAppear {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@

import SwiftUI

final class WindowOverlayHostingController<Content>: UIHostingController<Content> where Content: View {
final class WindowOverlayHostingController<Content>: UIHostingController<Content>, DynamicProperty where Content: View {
var key: WindowKey
var builder: () -> Content

init(key: WindowKey, rootView: Content) {
init(key: WindowKey, builder: @escaping () -> Content) {
self.key = key
super.init(rootView: rootView)
self.builder = builder
super.init(rootView: builder())
}

func update() {
rootView = builder()
}

@available(*, unavailable)
Expand Down

0 comments on commit 7f1c1c3

Please sign in to comment.