Skip to content

Commit 2b251c2

Browse files
committed
Initial implementation
1 parent 3ad5a37 commit 2b251c2

File tree

9 files changed

+168
-35
lines changed

9 files changed

+168
-35
lines changed

Package.swift

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
1-
// swift-tools-version: 5.6
2-
// The swift-tools-version declares the minimum version of Swift required to build this package.
1+
// swift-tools-version:5.1
32

43
import PackageDescription
54

65
let package = Package(
76
name: "FullScreenOverlay",
7+
platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6)],
88
products: [
9-
// Products define the executables and libraries a package produces, and make them visible to other packages.
10-
.library(
11-
name: "FullScreenOverlay",
12-
targets: ["FullScreenOverlay"]),
13-
],
14-
dependencies: [
15-
// Dependencies declare other packages that this package depends on.
16-
// .package(url: /* package url */, from: "1.0.0"),
9+
.library(name: "FullScreenOverlay", targets: ["FullScreenOverlay", "Previews"])
1710
],
1811
targets: [
19-
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
20-
// Targets can depend on other targets in this package, and on products in packages this package depends on.
21-
.target(
22-
name: "FullScreenOverlay",
23-
dependencies: []),
24-
.testTarget(
25-
name: "FullScreenOverlayTests",
26-
dependencies: ["FullScreenOverlay"]),
12+
.target(name: "FullScreenOverlay", dependencies: []),
13+
.target(name: "Previews", dependencies: ["FullScreenOverlay"])
2714
]
2815
)

Sources/FullScreenOverlay/FullScreenOverlay.swift

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import SwiftUI
2+
3+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
4+
class FullScreenOverlayContainer: ObservableObject {
5+
6+
static let shared: FullScreenOverlayContainer = .init()
7+
8+
@Published private(set) var overlays: [PresentationSpace: [AnyHashable: AnyView]] = .init()
9+
10+
func updateOverlay<ID: Hashable, Overlay: View>(_ overlay: Overlay, for id: ID, in presentationSpace: PresentationSpace) {
11+
self.overlays[presentationSpace, default: [:]].updateValue(AnyView(overlay), forKey: id)
12+
}
13+
14+
func removeOverlay<ID: Hashable>(for id: ID, in presentationSpace: PresentationSpace) {
15+
self.overlays[presentationSpace, default: [:]].removeValue(forKey: id)
16+
}
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import SwiftUI
2+
3+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
4+
struct FullScreenOverlayPresenter: ViewModifier {
5+
6+
var presentationSpace: PresentationSpace
7+
8+
@ObservedObject private var container: FullScreenOverlayContainer = .shared
9+
10+
func body(content: Content) -> some View {
11+
content.overlay(
12+
ZStack {
13+
ForEach(
14+
container.overlays[presentationSpace, default: [:]].sorted(by: { $0.key.hashValue < $1.key.hashValue }),
15+
id: \.key
16+
) { _, overlay in
17+
overlay
18+
}
19+
}
20+
)
21+
}
22+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import SwiftUI
2+
3+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
4+
struct FullScreenOverlaySetter<Overlay: View>: ViewModifier {
5+
6+
var overlay: Overlay
7+
var presentationSpace: PresentationSpace
8+
9+
@State private var id: UUID = .init()
10+
@State private var isAppearing: Bool = false
11+
12+
func body(content: Content) -> some View {
13+
return content
14+
.onAppear { isAppearing = true; updateOverlay() }
15+
.onDisappear { isAppearing = false; removeOverlay() }
16+
.onUpdate { isAppearing ? updateOverlay() : removeOverlay() }
17+
// onDisappear 이후에 onUpdate가 실행되는 경우도 있기 때문에, isAppearing 상태를 저장해 오버레이가 다시 추가되는 것을 방지한다.
18+
}
19+
20+
private func updateOverlay() {
21+
FullScreenOverlayContainer.shared.updateOverlay(overlay, for: id, in: presentationSpace)
22+
}
23+
24+
private func removeOverlay() {
25+
FullScreenOverlayContainer.shared.removeOverlay(for: id, in: presentationSpace)
26+
}
27+
}
28+
29+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
30+
private extension View {
31+
32+
func onUpdate(perform action: (() -> Void)? = nil) -> some View {
33+
if let action = action {
34+
DispatchQueue.main.async(execute: action)
35+
}
36+
return self
37+
}
38+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import SwiftUI
2+
3+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
4+
public enum PresentationSpace: Equatable, Hashable {
5+
case named(AnyHashable)
6+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import SwiftUI
2+
3+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
4+
public extension View {
5+
6+
func fullScreenOverlayPresentationSpace<T: Hashable>(name: T) -> some View {
7+
self
8+
.modifier(FullScreenOverlayPresenter(presentationSpace: .named(name)))
9+
}
10+
11+
@ViewBuilder func fullScreenOverlay<Overlay: View>(
12+
presentationSpace: PresentationSpace,
13+
@ViewBuilder content: () -> Overlay
14+
) -> some View {
15+
self.modifier(FullScreenOverlaySetter(overlay: content(), presentationSpace: presentationSpace))
16+
}
17+
}

Sources/Previews/Previews.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import SwiftUI
2+
import FullScreenOverlay
3+
4+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
5+
private struct RootView: View {
6+
7+
@State private var isPresentingBottomSheet: Bool = false
8+
9+
var body: some View {
10+
List {
11+
Button(
12+
"Present Bottom Sheet",
13+
action: { isPresentingBottomSheet = true }
14+
)
15+
.fullScreenOverlay(presentationSpace: .named("RootView")) {
16+
if isPresentingBottomSheet {
17+
BottomSheet(onDismiss: { isPresentingBottomSheet = false })
18+
}
19+
}
20+
}
21+
}
22+
}
23+
24+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
25+
private struct BottomSheet: View {
26+
27+
var onDismiss: () -> Void
28+
29+
@State private var isAppearing: Bool = false
30+
31+
var body: some View {
32+
Color.black.opacity(0.2)
33+
.edgesIgnoringSafeArea(.all)
34+
.zIndex(-1)
35+
.onTapGesture(perform: onDismiss)
36+
.transition(.opacity.animation(.default))
37+
VStack {
38+
Text("Bottom Sheet")
39+
.font(.title.weight(.semibold))
40+
.padding(.vertical, 128)
41+
Button("Dismiss", action: { isAppearing = false; onDismiss() })
42+
}
43+
.padding(.vertical, 32)
44+
.frame(maxWidth: .infinity)
45+
.background(
46+
Color.white
47+
.shadow(radius: 1)
48+
.edgesIgnoringSafeArea(.all)
49+
)
50+
.frame(maxHeight: .infinity, alignment: .bottom)
51+
.transition(.move(edge: .bottom))
52+
.animation(.spring())
53+
}
54+
}
55+
56+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
57+
internal struct SwiftUIView_Previews: PreviewProvider {
58+
59+
static var previews: some View {
60+
RootView()
61+
.fullScreenOverlayPresentationSpace(name: "RootView")
62+
}
63+
}

Tests/FullScreenOverlayTests/FullScreenOverlayTests.swift

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)