Skip to content

Commit

Permalink
Patch 2.5.1
Browse files Browse the repository at this point in the history
fix:
- Fixed a problem with native sheets covering popups (#110)
- Fixed a problem with popups retaining previous @State object (#101)
- Fixed a problem with KeyboardManager publishing its state outside the main thread (#120)
- Fixed a problem with PopupView that was changing the position of elements (#122)
  • Loading branch information
FulcrumOne committed Aug 3, 2024
1 parent 1412c1f commit 56c06db
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 27 deletions.
2 changes: 1 addition & 1 deletion MijickPopupView.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Pod::Spec.new do |s|
PopupView is a free and open-source library dedicated for SwiftUI that makes the process of presenting popups easier and much cleaner.
DESC

s.version = '2.5.0'
s.version = '2.5.1'
s.ios.deployment_target = '14.0'
s.osx.deployment_target = '12.0'
s.swift_version = '5.0'
Expand Down
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

<p align="center">
<a href="https://github.com/Mijick/PopupView-Demo" rel="nofollow">Try demo we prepared</a>
|
<a href="https://mijick.notion.site/c95fe641ff684561a4523b9570113516?v=46960f9a652147d4a12fc9dd36cbd4fc" rel="nofollow">Roadmap</a>
|
<a href="https://github.com/Mijick/PopupView/issues/new" rel="nofollow">Propose a new feature</a>
</p>

<br>
Expand Down Expand Up @@ -105,11 +109,49 @@ Installation steps:

# Usage
### 1. Setup library
Inside your `@main` structure call the `implementPopupView` method. It takes the optional argument - *config*, that can be used to configure some modifiers for all popups in the application.
The library can be initialised in either of two ways:
1. **DOES NOT WORK with SwiftUI sheets**<br>Inside your @main structure call the implementPopupView method. It takes the optional argument - config, that can be used to configure some modifiers for all popups in the application.
```Swift
var body: some Scene {
@main struct PopupView_Main: App {
var body: some Scene {
WindowGroup(content: ContentView().implementPopupView)
}
}
}
```
2. **WORKS with SwiftUI sheets. Only for iOS**<br>
Declare an AppDelegate class conforming to UIApplicationDelegate and add it to the @main structure.
```Swift
@main struct PopupView_Main: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

var body: some Scene { WindowGroup(content: ContentView.init) }
}

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
sceneConfig.delegateClass = CustomPopupSceneDelegate.self
return sceneConfig
}
}

class CustomPopupSceneDelegate: PopupSceneDelegate {
override init() {
super.init()
config = { $0
.top { $0
.cornerRadius(24)
.dragGestureEnabled(true)
}
.centre { $0
.tapOutsideToDismiss(false)
}
.bottom { $0
.stackLimit(5)
}
}
}
}
```

### 2. Declare a structure of your popup
Expand Down
5 changes: 2 additions & 3 deletions Sources/Internal/Extensions/View.ScreenManager++.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import SwiftUI

extension View {
func updateScreenSize() -> some View { GeometryReader { reader in
frame(maxWidth: .infinity, maxHeight: .infinity)
frame(width: reader.size.width, height: reader.size.height).frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear { ScreenManager.update(reader) }
.onChange(of: reader.size) { _ in ScreenManager.update(reader) }
.onChange(of: reader.safeAreaInsets) { _ in ScreenManager.update(reader) }
.onChange(of: reader.frame(in: .global)) { _ in ScreenManager.update(reader) }
}}
}
fileprivate extension ScreenManager {
Expand Down
5 changes: 4 additions & 1 deletion Sources/Internal/Managers/KeyboardManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ class KeyboardManager: ObservableObject {
private init() { subscribeToKeyboardEvents() }
}
extension KeyboardManager {
static func hideKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
static func hideKeyboard() { DispatchQueue.main.async {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}}
}

private extension KeyboardManager {
func subscribeToKeyboardEvents() {
Publishers.Merge(getKeyboardWillOpenPublisher(), createKeyboardWillHidePublisher())
.receive(on: DispatchQueue.main)
.sink { self.height = $0 }
.store(in: &subscription)
}
Expand Down
16 changes: 8 additions & 8 deletions Sources/Internal/Managers/PopupManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import SwiftUI
public class PopupManager: ObservableObject {
@Published private(set) var views: [any Popup] = []
private(set) var presenting: Bool = true
private(set) var popupsWithoutOverlay: [String] = []
private(set) var popupsToBeDismissed: [String: DispatchSourceTimer] = [:]
private(set) var popupsWithoutOverlay: [ID] = []
private(set) var popupsToBeDismissed: [ID: DispatchSourceTimer] = [:]

static let shared: PopupManager = .init()
private init() {}
Expand All @@ -23,20 +23,20 @@ public class PopupManager: ObservableObject {
// MARK: - Operations
enum StackOperation {
case insertAndReplace(any Popup), insertAndStack(any Popup)
case removeLast, remove(id: String), removeAllUpTo(id: String), removeAll
case removeLast, remove(ID), removeAllUpTo(ID), removeAll
}
extension PopupManager {
static func performOperation(_ operation: StackOperation) { DispatchQueue.main.async {
removePopupFromStackToBeDismissed(operation)
updateOperationType(operation)
shared.views.perform(operation)
}}
static func dismissPopupAfter(_ popup: any Popup, _ seconds: Double) { shared.popupsToBeDismissed[popup.id] = DispatchSource.createAction(deadline: seconds) { performOperation(.remove(id: popup.id)) } }
static func dismissPopupAfter(_ popup: any Popup, _ seconds: Double) { shared.popupsToBeDismissed[popup.id] = DispatchSource.createAction(deadline: seconds) { performOperation(.remove(popup.id)) } }
static func hideOverlay(_ popup: any Popup) { shared.popupsWithoutOverlay.append(popup.id) }
}
private extension PopupManager {
static func removePopupFromStackToBeDismissed(_ operation: StackOperation) { switch operation {
case .removeLast: shared.popupsToBeDismissed.removeValue(forKey: shared.views.last?.id ?? "")
case .removeLast: shared.popupsToBeDismissed.removeValue(forKey: shared.views.last?.id ?? .init())
case .remove(let id): shared.popupsToBeDismissed.removeValue(forKey: id)
case .removeAllUpTo, .removeAll: shared.popupsToBeDismissed.removeAll()
default: break
Expand All @@ -60,12 +60,12 @@ private extension [any Popup] {
case .insertAndReplace(let popup): replaceLast(popup, if: canBeInserted(popup))
case .insertAndStack(let popup): append(popup, if: canBeInserted(popup))
case .removeLast: removeLast()
case .remove(let id): removeAll(where: { $0.id == id })
case .removeAllUpTo(let id): removeAllUpToElement(where: { $0.id == id })
case .remove(let id): removeAll(where: { $0.id ~= id })
case .removeAllUpTo(let id): removeAllUpToElement(where: { $0.id ~= id })
case .removeAll: removeAll()
}
}
}
private extension [any Popup] {
func canBeInserted(_ popup: some Popup) -> Bool { !contains(where: { $0.id == popup.id }) }
func canBeInserted(_ popup: some Popup) -> Bool { !contains(where: { $0.id ~= popup.id }) }
}
2 changes: 1 addition & 1 deletion Sources/Internal/Other/AnyPopup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import SwiftUI

struct AnyPopup<Config: Configurable>: Popup, Hashable {
let id: String
let id: ID
private let _body: AnyView
private let _configBuilder: (Config) -> Config

Expand Down
43 changes: 43 additions & 0 deletions Sources/Internal/Other/ID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// ID.swift of PopupView
//
// Created by Tomasz Kurylik
// - Twitter: https://twitter.com/tkurylik
// - Mail: [email protected]
// - GitHub: https://github.com/FulcrumOne
//
// Copyright ©2024 Mijick. Licensed under MIT License.


import Foundation

public struct ID {
let value: String
}

// MARK: - Initialisers
extension ID {
init<P: Popup>(_ object: P) { self.init(P.self) }
init<P: Popup>(_ type: P.Type) { self.value = .init(describing: P.self) + Self.separator + .init(describing: Date()) }
init() { self.value = "" }
}

// MARK: - Equatable
extension ID: Equatable {
public static func ==(lhs: Self, rhs: Self) -> Bool { lhs.value == rhs.value }
public static func ~=(lhs: Self, rhs: Self) -> Bool { getComponent(lhs) == getComponent(rhs) }
}

// MARK: - Hashing
extension ID: Hashable {
public func hash(into hasher: inout Hasher) { hasher.combine(Self.getComponent(self)) }
}


// MARK: - Helpers
private extension ID {
static func getComponent(_ object: Self) -> String { object.value.components(separatedBy: separator).first ?? "" }
}
private extension ID {
static var separator: String { "/{}/" }
}
6 changes: 3 additions & 3 deletions Sources/Internal/Protocols/Popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ public protocol Popup: View {
associatedtype Config: Configurable
associatedtype V: View

var id: String { get }
var id: ID { get }

func createContent() -> V
func configurePopup(popup: Config) -> Config
}
public extension Popup {
var id: String { .init(describing: Self.self) }
var id: ID { .init(self) }
var body: V { createContent() }

func configurePopup(popup: Config) -> Config { popup }
}

// MARK: - Helpers
extension Popup {
func remove() { PopupManager.performOperation(.remove(id: id)) }
func remove() { PopupManager.performOperation(.remove(id)) }
}
4 changes: 2 additions & 2 deletions Sources/Internal/Protocols/PopupStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ protocol PopupStack: View {
associatedtype Config: Configurable

var items: [AnyPopup<Config>] { get }
var heights: [String: CGFloat] { get }
var heights: [ID: CGFloat] { get }
var globalConfig: GlobalConfig { get }
var gestureTranslation: CGFloat { get }
var isGestureActive: Bool { get }
Expand All @@ -29,7 +29,7 @@ protocol PopupStack: View {
var tapOutsideClosesPopup: Bool { get }
}
extension PopupStack {
var heights: [String: CGFloat] { [:] }
var heights: [ID: CGFloat] { [:] }
var gestureTranslation: CGFloat { 0 }
var isGestureActive: Bool { false }
var translationProgress: CGFloat { 1 }
Expand Down
2 changes: 1 addition & 1 deletion Sources/Internal/Views/PopupBottomStackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct PopupBottomStackView: PopupStack {
let items: [AnyPopup<BottomPopupConfig>]
let globalConfig: GlobalConfig
@State var gestureTranslation: CGFloat = 0
@State var heights: [String: CGFloat] = [:]
@State var heights: [ID: CGFloat] = [:]
@GestureState var isGestureActive: Bool = false
@ObservedObject private var screenManager: ScreenManager = .shared
@ObservedObject private var keyboardManager: KeyboardManager = .shared
Expand Down
2 changes: 1 addition & 1 deletion Sources/Internal/Views/PopupTopStackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct PopupTopStackView: PopupStack {
let items: [AnyPopup<TopPopupConfig>]
let globalConfig: GlobalConfig
@State var gestureTranslation: CGFloat = 0
@State var heights: [String: CGFloat] = [:]
@State var heights: [ID: CGFloat] = [:]
@GestureState var isGestureActive: Bool = false
@ObservedObject private var screenManager: ScreenManager = .shared

Expand Down
2 changes: 1 addition & 1 deletion Sources/Internal/Views/PopupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private extension PopupView {
func isOverlayActive<P: Popup>(_ type: P.Type) -> Bool { popupManager.views.last is P && !shouldOverlayBeHiddenForCurrentPopup }
}
private extension PopupView {
var shouldOverlayBeHiddenForCurrentPopup: Bool { popupManager.popupsWithoutOverlay.contains(popupManager.views.last?.id ?? "") }
var shouldOverlayBeHiddenForCurrentPopup: Bool { popupManager.popupsWithoutOverlay.contains(popupManager.views.last?.id ?? .init()) }
}

private extension PopupView {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Public/Extensions/Public+PopupManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ public extension PopupManager {
static func dismiss() { performOperation(.removeLast) }

/// Dismisses all popups of provided type on the stack.
static func dismiss<P: Popup>(_ popup: P.Type) { performOperation(.remove(id: .init(describing: popup))) }
static func dismiss<P: Popup>(_ popup: P.Type) { performOperation(.remove(ID(popup))) }

/// Dismisses all popups on the stack up to the popup with the selected type
static func dismissAll<P: Popup>(upTo popup: P.Type) { performOperation(.removeAllUpTo(id: .init(describing: popup))) }
static func dismissAll<P: Popup>(upTo popup: P.Type) { performOperation(.removeAllUpTo(ID(popup))) }

/// Dismisses all the popups on the stack.
static func dismissAll() { performOperation(.removeAll) }
Expand Down
44 changes: 44 additions & 0 deletions Sources/Public/Public+PopupSceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// Public+PopupSceneDelegate.swift of PopupView
//
// Created by Tomasz Kurylik
// - Twitter: https://twitter.com/tkurylik
// - Mail: [email protected]
// - GitHub: https://github.com/FulcrumOne
//
// Copyright ©2024 Mijick. Licensed under MIT License.


import SwiftUI

#if os(iOS)
open class PopupSceneDelegate: NSObject, UIWindowSceneDelegate {
open var window: UIWindow?
open var config: (GlobalConfig) -> (GlobalConfig) = { _ in .init() }
}

// MARK: - Creating Popup Scene
extension PopupSceneDelegate {
open func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let windowScene = scene as? UIWindowScene {
let hostingController = UIHostingController(rootView: Rectangle()
.fill(Color.clear)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.implementPopupView(config: config)
)
hostingController.view.backgroundColor = .clear

window = Window(windowScene: windowScene)
window?.rootViewController = hostingController
window?.isHidden = false
}}
}


// MARK: - Helpers
fileprivate class Window: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, with: event) else { return nil }
return rootViewController?.view == view ? nil : view
}
}
#endif

0 comments on commit 56c06db

Please sign in to comment.