Skip to content

Commit

Permalink
Merge pull request #2 from aheze/feature/embed-view-into-parent
Browse files Browse the repository at this point in the history
Add VoiceOver and resizing support
  • Loading branch information
ShezHsky committed Jan 17, 2022
2 parents d3adcb6 + 6027bc6 commit e8ca4b2
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 130 deletions.
16 changes: 8 additions & 8 deletions Examples/PopoversPlaygroundsApp.swiftpm/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
// This file is automatically generated.
// Do not edit it by hand because the contents will be replaced.

import PackageDescription
import AppleProductTypes
import PackageDescription

let package = Package(
name: "PopoversPlaygroundsApp",
platforms: [
.iOS("15.2")
.iOS("15.2"),
],
products: [
.iOSApplication(
Expand All @@ -24,26 +24,26 @@ let package = Package(
accentColorAssetName: "AccentColor",
supportedDeviceFamilies: [
.pad,
.phone
.phone,
],
supportedInterfaceOrientations: [
.portrait,
.landscapeRight,
.landscapeLeft,
.portraitUpsideDown(.when(deviceFamilies: [.pad]))
.portraitUpsideDown(.when(deviceFamilies: [.pad])),
]
)
),
],
dependencies: [
.package(url: "https://github.com/aheze/Popovers", "1.0.0"..<"2.0.0")
.package(url: "https://github.com/aheze/Popovers", "1.0.0" ..< "2.0.0"),
],
targets: [
.executableTarget(
name: "AppModule",
dependencies: [
.product(name: "Popovers", package: "popovers")
.product(name: "Popovers", package: "popovers"),
],
path: "."
)
),
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
3CA34FAC279533E300AC36DF /* AccessibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA34FAB279533E300AC36DF /* AccessibilityView.swift */; };
3CBD875E27755E45005BBA48 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBD875D27755E45005BBA48 /* App.swift */; };
3CBD876527755E50005BBA48 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBD875F27755E50005BBA48 /* ContentView.swift */; };
3CBD876627755E50005BBA48 /* UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBD876027755E50005BBA48 /* UIKit.swift */; };
Expand Down Expand Up @@ -44,6 +45,7 @@

/* Begin PBXFileReference section */
3C6C745127822EE600E039F0 /* Popovers */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Popovers; path = ../..; sourceTree = "<group>"; };
3CA34FAB279533E300AC36DF /* AccessibilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityView.swift; sourceTree = "<group>"; };
3CBD874C27755E19005BBA48 /* PopoversXcodeApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PopoversXcodeApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
3CBD875D27755E45005BBA48 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
3CBD875F27755E50005BBA48 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -133,6 +135,7 @@
3CBD876F27755E57005BBA48 /* NestedView.swift */,
3CBD877027755E57005BBA48 /* SelectionView.swift */,
3CBD877127755E57005BBA48 /* BasicView.swift */,
3CA34FAB279533E300AC36DF /* AccessibilityView.swift */,
3CBD877227755E57005BBA48 /* BackgroundView.swift */,
3CBD877327755E57005BBA48 /* RelativePositioningView.swift */,
3CBD877427755E57005BBA48 /* PopoverReaderView.swift */,
Expand Down Expand Up @@ -269,6 +272,7 @@
3CBD876627755E50005BBA48 /* UIKit.swift in Sources */,
3CBD877E27755E57005BBA48 /* BackgroundView.swift in Sources */,
3CBD876A27755E50005BBA48 /* Showroom.swift in Sources */,
3CA34FAC279533E300AC36DF /* AccessibilityView.swift in Sources */,
3CBD877F27755E57005BBA48 /* RelativePositioningView.swift in Sources */,
3CBD876727755E50005BBA48 /* Utilities.swift in Sources */,
3CBD879027755E5B005BBA48 /* VideoView.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions Examples/PopoversXcodeApp/PopoversXcodeApp/Playground.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ struct Playground: View {
NestedView()
SelectionView()
}

Group {
AccessibilityView()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// AccessibilityView.swift
// PopoversXcodeApp
//
// Created by A. Zheng (github.com/aheze) on 1/16/22.
// Copyright © 2022 A. Zheng. All rights reserved.
//

import Popovers
import SwiftUI

struct AccessibilityView: View {
@State var present = false

var body: some View {
ExampleRow(
image: "hand.point.up.braille",
title: "Accessibility",
color: 0x0021FF
) {
present.toggle()
}
.popover(
present: $present,
attributes: {
$0.accessibility.shiftFocus = false
$0.accessibility.dismissButtonLabel = AnyView(
Text("Tap me to dismiss!")
.foregroundColor(.white)
.padding()
.background(Color.black.opacity(0.5))
.cornerRadius(16)
)
}
) {
VStack {
VStack(alignment: .leading) {
Text("Popovers has full VoiceOver support!")

HStack {
ExampleImage("speaker.wave.2", color: 0x0021FF)

Text("By default, VoiceOver will read out the popover when it's presented. You can change this with `attributes.accessibility.shiftFocus`.")
}

HStack {
ExampleImage("hand.thumbsup", color: 0x0021FF)

Text("By default, a \(Image(systemName: "xmark.circle.fill")) button will appear next to popovers when VoiceOver is on. You can customize this with `attributes.accessibility.dismissButtonLabel`.")
}

HStack {
ExampleImage.tip

Text("If you already have a button that sets `present` to `false`, remove the default dismiss button with `dismissButtonLabel = nil`.")
}
}
}
.padding()
.background(.background)
.cornerRadius(12)
.shadow(radius: 1)
.frame(maxWidth: 500)
}
}
}
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "Popovers",
platforms: [
.iOS(.v13)
.iOS(.v13),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
Expand All @@ -26,6 +26,6 @@ let package = Package(
name: "Popovers",
dependencies: [],
path: "Sources"
)
),
]
)
59 changes: 37 additions & 22 deletions Sources/Popover+Lifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,51 @@ public extension Popover {

/// Inject the transaction into the popover, so following frame calculations are animated smoothly.
context.transaction = transaction

/// Get the popover model that's tied to the window.
let model = window.popoverModel

/**
Add the popover to the container view.
*/
let displayPopover: () -> Void = {
func displayPopover(in container: PopoverGestureContainer) {
withTransaction(transaction) {
model.add(self)

/// Stop VoiceOver from reading out background views if `blocksBackgroundTouches` is true.
if attributes.blocksBackgroundTouches {
container.accessibilityViewIsModal = true
}

/// Shift VoiceOver focus to the popover.
if attributes.accessibility.shiftFocus {
UIAccessibility.post(notification: .screenChanged, argument: nil)
}
}
}

/// Find the existing container view for popovers in this window. If it does not exist, we need to insert one.
let container: PopoverGestureContainer
if let existingContainer = window.popoverContainerView {
container = existingContainer

/// The container is already laid out in the window, so we can go ahead and show the popover.
displayPopover()
displayPopover(in: container)
} else {
container = PopoverGestureContainer(frame: window.bounds)

/// Wait until the container is present in the view hiearchy before showing the popover, otherwise all the
/// layout math will be working with wonky frames.
container.onMovedToWindow = displayPopover


/**
Wait until the container is present in the view hierarchy before showing the popover,
otherwise all the layout math will be working with wonky frames.
*/
container.onMovedToWindow = {
displayPopover(in: container)
}

window.addSubview(container)
}
/// Hang onto the container for future dismiss/replace actions.

/// Hang on to the container for future dismiss/replace actions.
context.presentedPopoverContainer = container
}

Expand All @@ -78,6 +92,9 @@ public extension Popover {
context.presentedPopoverContainer?.removeFromSuperview()
context.presentedPopoverContainer = nil
}

/// If at least one popover has `blocksBackgroundTouches` set to true, stop VoiceOver from reading out background views
context.presentedPopoverContainer?.accessibilityViewIsModal = model.popovers.contains { $0.attributes.blocksBackgroundTouches }
}

/// Remove this popover from the view model, dismissing it.
Expand Down Expand Up @@ -122,28 +139,27 @@ public extension Popover {
}
}

extension UIResponder {
public extension UIResponder {
/// Replace a popover with another popover. Convenience method for `Popover.replace(with:)`.
public func replace(_ oldPopover: Popover, with newPopover: Popover) {
func replace(_ oldPopover: Popover, with newPopover: Popover) {
oldPopover.replace(with: newPopover)
}

/// Dismiss a popover. Convenience method for `Popover.dismiss(transaction:)`.
public func dismiss(_ popover: Popover) {
func dismiss(_ popover: Popover) {
popover.dismiss()
}
}

extension UIViewController {
public extension UIViewController {
/// Present a `Popover` using this `UIViewController` as its presentation context.
public func present(_ popover: Popover) {
func present(_ popover: Popover) {
guard let window = view.window else { return }
popover.present(in: window)
}
}

extension UIView {

var popoverContainerView: PopoverGestureContainer? {
if let container = self as? PopoverGestureContainer {
return container
Expand All @@ -153,9 +169,8 @@ extension UIView {
return container
}
}

return nil
}
}

}
44 changes: 42 additions & 2 deletions Sources/Popover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ public struct Popover: Identifiable {
/// Prevent views underneath the popover from being pressed.
public var blocksBackgroundTouches = false

/// Stores accessibility values.
public var accessibility = Accessibility()

/// Called when the user taps outside the popover.
public var onTapOutside: (() -> Void)?

Expand All @@ -125,6 +128,7 @@ public struct Popover: Identifiable {
dismissal: Popover.Attributes.Dismissal = Dismissal(),
rubberBandingMode: Popover.Attributes.RubberBandingMode = [.xAxis, .yAxis],
blocksBackgroundTouches: Bool = false,
accessibility: Accessibility = Accessibility(),
onTapOutside: (() -> Void)? = nil,
onDismiss: (() -> Void)? = nil,
onContextChange: ((Popover.Context) -> Void)? = nil
Expand All @@ -138,6 +142,7 @@ public struct Popover: Identifiable {
self.dismissal = dismissal
self.rubberBandingMode = rubberBandingMode
self.blocksBackgroundTouches = blocksBackgroundTouches
self.accessibility = accessibility
self.onTapOutside = onTapOutside
self.onDismiss = onDismiss
self.onContextChange = onContextChange
Expand Down Expand Up @@ -329,6 +334,39 @@ public struct Popover: Identifiable {
public static let none = Mode([])
}
}

/// Define VoiceOver behavior.
public struct Accessibility {
/// Focus the popover when presented.
public var shiftFocus = true

/**
A view that's only shown when VoiceOver is running. Dismisses the popover when tapped.
Tap-outside-to-dismiss is unsupported in VoiceOver, so this provides an alternate method for dismissal.
*/
public var dismissButtonLabel: AnyView? = defaultDismissButtonLabel

/// Create the default VoiceOver behavior for the popover.
public init(
shiftFocus: Bool = true,
dismissButtonLabel: (() -> AnyView)? = { defaultDismissButtonLabel }
) {
self.shiftFocus = shiftFocus
self.dismissButtonLabel = dismissButtonLabel?()
}

/// The default voiceover dismiss button view, an X
public static let defaultDismissButtonLabel: AnyView = .init(
AnyView(
Image(systemName: "xmark")
.foregroundColor(.white)
.frame(width: 36, height: 36)
.background(Color.black.opacity(0.25))
.cornerRadius(18)
)
)
}
}

/**
Expand Down Expand Up @@ -373,7 +411,7 @@ public struct Popover: Identifiable {
if let window = presentedPopoverContainer?.window {
return window
} else {
print("This popover is not tied to a window. Please file a bug report (https://github.com/aheze/Popovers/issues)")
print("[Popovers] - This popover is not tied to a window. Please file a bug report (https://github.com/aheze/Popovers/issues).")
return UIWindow()
}
}
Expand Down Expand Up @@ -407,7 +445,9 @@ public struct Popover: Identifiable {
public init() {
changeSink = objectWillChange.sink { [weak self] in
guard let self = self else { return }
self.attributes.onContextChange?(self)
DispatchQueue.main.async {
self.attributes.onContextChange?(self)
}
}
}
}
Expand Down

0 comments on commit e8ca4b2

Please sign in to comment.