diff --git a/Package.swift b/Package.swift index 90e206b..f2a2f60 100644 --- a/Package.swift +++ b/Package.swift @@ -1,28 +1,15 @@ -// swift-tools-version: 5.6 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:5.1 import PackageDescription let package = Package( name: "FullScreenOverlay", + platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6)], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "FullScreenOverlay", - targets: ["FullScreenOverlay"]), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), + .library(name: "FullScreenOverlay", targets: ["FullScreenOverlay", "Previews"]) ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "FullScreenOverlay", - dependencies: []), - .testTarget( - name: "FullScreenOverlayTests", - dependencies: ["FullScreenOverlay"]), + .target(name: "FullScreenOverlay", dependencies: []), + .target(name: "Previews", dependencies: ["FullScreenOverlay"]) ] ) diff --git a/Sources/FullScreenOverlay/FullScreenOverlay.swift b/Sources/FullScreenOverlay/FullScreenOverlay.swift deleted file mode 100644 index 6e24bfc..0000000 --- a/Sources/FullScreenOverlay/FullScreenOverlay.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct FullScreenOverlay { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Sources/FullScreenOverlay/FullScreenOverlayContainer.swift b/Sources/FullScreenOverlay/FullScreenOverlayContainer.swift new file mode 100644 index 0000000..f55b936 --- /dev/null +++ b/Sources/FullScreenOverlay/FullScreenOverlayContainer.swift @@ -0,0 +1,17 @@ +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +class FullScreenOverlayContainer: ObservableObject { + + static let shared: FullScreenOverlayContainer = .init() + + @Published private(set) var overlays: [PresentationSpace: [AnyHashable: AnyView]] = .init() + + func updateOverlay(_ overlay: Overlay, for id: ID, in presentationSpace: PresentationSpace) { + self.overlays[presentationSpace, default: [:]].updateValue(AnyView(overlay), forKey: id) + } + + func removeOverlay(for id: ID, in presentationSpace: PresentationSpace) { + self.overlays[presentationSpace, default: [:]].removeValue(forKey: id) + } +} diff --git a/Sources/FullScreenOverlay/FullScreenOverlayPresenter.swift b/Sources/FullScreenOverlay/FullScreenOverlayPresenter.swift new file mode 100644 index 0000000..648431d --- /dev/null +++ b/Sources/FullScreenOverlay/FullScreenOverlayPresenter.swift @@ -0,0 +1,22 @@ +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +struct FullScreenOverlayPresenter: ViewModifier { + + var presentationSpace: PresentationSpace + + @ObservedObject private var container: FullScreenOverlayContainer = .shared + + func body(content: Content) -> some View { + content.overlay( + ZStack { + ForEach( + container.overlays[presentationSpace, default: [:]].sorted(by: { $0.key.hashValue < $1.key.hashValue }), + id: \.key + ) { _, overlay in + overlay + } + } + ) + } +} diff --git a/Sources/FullScreenOverlay/FullScreenOverlaySetter.swift b/Sources/FullScreenOverlay/FullScreenOverlaySetter.swift new file mode 100644 index 0000000..0242cee --- /dev/null +++ b/Sources/FullScreenOverlay/FullScreenOverlaySetter.swift @@ -0,0 +1,38 @@ +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +struct FullScreenOverlaySetter: ViewModifier { + + var overlay: Overlay + var presentationSpace: PresentationSpace + + @State private var id: UUID = .init() + @State private var isAppearing: Bool = false + + func body(content: Content) -> some View { + return content + .onAppear { isAppearing = true; updateOverlay() } + .onDisappear { isAppearing = false; removeOverlay() } + .onUpdate { isAppearing ? updateOverlay() : removeOverlay() } + // onDisappear 이후에 onUpdate가 실행되는 경우도 있기 때문에, isAppearing 상태를 저장해 오버레이가 다시 추가되는 것을 방지한다. + } + + private func updateOverlay() { + FullScreenOverlayContainer.shared.updateOverlay(overlay, for: id, in: presentationSpace) + } + + private func removeOverlay() { + FullScreenOverlayContainer.shared.removeOverlay(for: id, in: presentationSpace) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private extension View { + + func onUpdate(perform action: (() -> Void)? = nil) -> some View { + if let action = action { + DispatchQueue.main.async(execute: action) + } + return self + } +} diff --git a/Sources/FullScreenOverlay/PresentationSpace.swift b/Sources/FullScreenOverlay/PresentationSpace.swift new file mode 100644 index 0000000..b89bd10 --- /dev/null +++ b/Sources/FullScreenOverlay/PresentationSpace.swift @@ -0,0 +1,6 @@ +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public enum PresentationSpace: Equatable, Hashable { + case named(AnyHashable) +} diff --git a/Sources/FullScreenOverlay/View+FullScreenOverlay.swift b/Sources/FullScreenOverlay/View+FullScreenOverlay.swift new file mode 100644 index 0000000..8559d6b --- /dev/null +++ b/Sources/FullScreenOverlay/View+FullScreenOverlay.swift @@ -0,0 +1,17 @@ +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public extension View { + + func fullScreenOverlayPresentationSpace(name: T) -> some View { + self + .modifier(FullScreenOverlayPresenter(presentationSpace: .named(name))) + } + + @ViewBuilder func fullScreenOverlay( + presentationSpace: PresentationSpace, + @ViewBuilder content: () -> Overlay + ) -> some View { + self.modifier(FullScreenOverlaySetter(overlay: content(), presentationSpace: presentationSpace)) + } +} diff --git a/Sources/Previews/Previews.swift b/Sources/Previews/Previews.swift new file mode 100644 index 0000000..b9392fb --- /dev/null +++ b/Sources/Previews/Previews.swift @@ -0,0 +1,63 @@ +import SwiftUI +import FullScreenOverlay + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private struct RootView: View { + + @State private var isPresentingBottomSheet: Bool = false + + var body: some View { + List { + Button( + "Present Bottom Sheet", + action: { isPresentingBottomSheet = true } + ) + .fullScreenOverlay(presentationSpace: .named("RootView")) { + if isPresentingBottomSheet { + BottomSheet(onDismiss: { isPresentingBottomSheet = false }) + } + } + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private struct BottomSheet: View { + + var onDismiss: () -> Void + + @State private var isAppearing: Bool = false + + var body: some View { + Color.black.opacity(0.2) + .edgesIgnoringSafeArea(.all) + .zIndex(-1) + .onTapGesture(perform: onDismiss) + .transition(.opacity.animation(.default)) + VStack { + Text("Bottom Sheet") + .font(.title.weight(.semibold)) + .padding(.vertical, 128) + Button("Dismiss", action: { isAppearing = false; onDismiss() }) + } + .padding(.vertical, 32) + .frame(maxWidth: .infinity) + .background( + Color.white + .shadow(radius: 1) + .edgesIgnoringSafeArea(.all) + ) + .frame(maxHeight: .infinity, alignment: .bottom) + .transition(.move(edge: .bottom)) + .animation(.spring()) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +internal struct SwiftUIView_Previews: PreviewProvider { + + static var previews: some View { + RootView() + .fullScreenOverlayPresentationSpace(name: "RootView") + } +} diff --git a/Tests/FullScreenOverlayTests/FullScreenOverlayTests.swift b/Tests/FullScreenOverlayTests/FullScreenOverlayTests.swift deleted file mode 100644 index f86c4d6..0000000 --- a/Tests/FullScreenOverlayTests/FullScreenOverlayTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import XCTest -@testable import FullScreenOverlay - -final class FullScreenOverlayTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(FullScreenOverlay().text, "Hello, World!") - } -}