Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS 13 modal sheets (issue #41) #43

Merged
merged 3 commits into from
Jul 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
version: 2

common: &common
macos:
xcode: "11.0.0"

env:
global:
- LC_CTYPE=en_US.UTF-8
Expand All @@ -14,8 +18,7 @@ jobs:
- run: swiftlint --strict

test-iOS:
macos:
xcode: "10.2.0"
<<: *common
steps:
- checkout
- run:
Expand All @@ -28,8 +31,7 @@ jobs:
sh build.sh test-iOS

examples:
macos:
xcode: "10.2.0"
<<: *common
steps:
- checkout
- run:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.7.0
- [Big fix] Fix presentation lifecycle management on iOS 13 when swiping down a modal sheet
- [Addition] Expose a custom adaptive presentation delegate with reactive interface

# 1.6.1
- Update Flow dependency and pin it to a compatible version

Expand Down
14 changes: 13 additions & 1 deletion Examples/StylesAndOptions/Example/AppFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,25 @@ extension AppFlow: Presentable {
return containerController.present(Presentation(TestNavigationBarHiding(), style: .modal)).toVoid()
}

if options.contains(.allowSwipeDismissAlways) {
struct NavigationStack: Presentable {
func materialize() -> (UIViewController, Disposable) {
let (vc, _) = TapToDismiss().materialize()
vc.present(TapToDismiss())

return (vc, NilDisposer())
}
}
return containerController.present(NavigationStack(), style: style, options: options)
}

if let alertToPresent = alertToPresent {
presentation = .right(Presentation(alertToPresent,
style: style,
options: options,
configure: withDismiss))
} else {
presentation = .left(Presentation(TapToDismiss(),
presentation = .left(Presentation(TapToDismiss(showAlertOnDidAttemptToDismiss: options.contains(.showAlertOnDidAttemptToDismiss)),
style: style,
options: options,
configure: style.name == "default" ? { _, _ in } : withDismiss))
Expand Down
8 changes: 6 additions & 2 deletions Examples/StylesAndOptions/Example/ChooseOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct ChoosePresentationOptions { }

extension PresentationOptions {
static let navigationBarPreference = PresentationOptions()
static let showAlertOnDidAttemptToDismiss = PresentationOptions()
}

extension PresentationOptions {
Expand All @@ -38,8 +39,11 @@ extension PresentationOptions {
("Auto Pop (for navigation vc)", .autoPop),
("Auto Pop Successors (for navigation vc)", .autoPopSuccessors),
("Auto Pop Self And Successors (for navigation vc)", .autoPopSelfAndSuccessors),
("NavigationBar visibility preference", .navigationBarPreference)
]
("NavigationBar visibility preference", .navigationBarPreference),
("Show alert on swipe down to dismiss", .showAlertOnDidAttemptToDismiss),
("Embed in navigation and swipe down to dismiss", [.showAlertOnDidAttemptToDismiss, embedInNavigationController]),
("Allow swipe to dismiss always", [.allowSwipeDismissAlways, .defaults])
]
return DataSource(options: presentationOptions.map {
NamedPresentationOptions(name: $0.0, value: $0.1)
})
Expand Down
27 changes: 25 additions & 2 deletions Examples/StylesAndOptions/Example/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,43 @@ extension UIBarButtonItem {
}
}

struct TapToDismiss { }
struct TapToDismiss {
let showAlertOnDidAttemptToDismiss: Bool

init(showAlertOnDidAttemptToDismiss: Bool = false) {
self.showAlertOnDidAttemptToDismiss = showAlertOnDidAttemptToDismiss
}
}

extension TapToDismiss: Presentable {
public func materialize() -> (UIViewController, Future<()>) {
let vc = UIViewController()
if #available(iOS 13.0, *) {
#if compiler(>=5.1)
vc.isModalInPresentation = showAlertOnDidAttemptToDismiss
#endif
}
let button = UIButton()
button.setTitle("Tap To Dismiss", for: .normal)
button.backgroundColor = .blue
vc.view = button

return (vc, Future<()> { completion in
return button.onValue {
let bag = DisposeBag()
if #available(iOS 13.0, *) {
let delegate = CustomAdaptivePresentationDelegate()
bag.hold(delegate)

vc.customAdaptivePresentationDelegate = delegate
bag += delegate.didAttemptToDismissSignal.onValue { _ in
let alertAction = Alert<()>.Action(title: "OK", action: { })
vc.present(Alert(message: "Test alert", actions: [alertAction]))
}
}
bag += button.onValue {
completion(.success)
}
return bag
})
}
}
127 changes: 89 additions & 38 deletions Examples/StylesAndOptions/ExampleUITests/ExampleUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ExampleUITests: XCTestCase {
let style = "default"

verifyForAllContainerConfigurations {
showDismissablePresentation(style: style, option: "Default")
chooseStyleAndOption(style: style, option: "Default")

let isSideBySideSplitView = app.launchArguments.contains("UseSplitViewContainer") &&
UIDevice.current.userInterfaceIdiom == .pad
Expand All @@ -30,23 +30,23 @@ class ExampleUITests: XCTestCase {
XCTAssertTrue(initialScreenVisible)
}

showDismissablePresentation(style: style, option: "Default")
chooseStyleAndOption(style: style, option: "Default")
// completing the presentation doesn't dismiss the pushed vc automatically, we need to pass .autoPop for that
pressDismiss()
pressDismiss()
pressBack()
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Auto Pop Self And Successors (for navigation vc)")
chooseStyleAndOption(style: style, option: "Auto Pop Self And Successors (for navigation vc)")
pressDismiss()
XCTAssertTrue(initialScreenVisible)

// no special behaviour for navigation presentation
showDismissablePresentation(style: style, option: "Fail On Block (for modal/popover vc)")
chooseStyleAndOption(style: style, option: "Fail On Block (for modal/popover vc)")
pressBack()
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Unanimated")
chooseStyleAndOption(style: style, option: "Unanimated")
pressBack()
XCTAssertTrue(initialScreenVisible)
}
Expand All @@ -57,22 +57,22 @@ class ExampleUITests: XCTestCase {
let cancel = app.navigationBars["UIView"].buttons["Cancel"]

verifyForAllContainerConfigurations {
showDismissablePresentation(style: style, option: "Default")
chooseStyleAndOption(style: style, option: "Default")
XCTAssertFalse(cancel.exists)
pressDismiss()
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Embed In Navigation Controller")
chooseStyleAndOption(style: style, option: "Embed In Navigation Controller")
XCTAssertFalse(cancel.exists)
pressDismiss()
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Unanimated")
chooseStyleAndOption(style: style, option: "Unanimated")
XCTAssertFalse(cancel.exists)
pressDismiss()
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Fail On Block (for modal/popover vc)")
chooseStyleAndOption(style: style, option: "Fail On Block (for modal/popover vc)")
XCTAssertTrue(initialScreenVisible)
}
}
Expand All @@ -82,22 +82,22 @@ class ExampleUITests: XCTestCase {
let cancel = app.navigationBars["UIView"].buttons["Cancel"]

verifyForAllContainerConfigurations {
showDismissablePresentation(style: style, option: "Embed In Navigation Controller")
cancel.tap()
chooseStyleAndOption(style: style, option: "Embed In Navigation Controller")
cancel.waitForExistenceAndTap()
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Embed In Navigation Controller")
chooseStyleAndOption(style: style, option: "Embed In Navigation Controller")
pressDismiss()
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Default")
cancel.tap()
chooseStyleAndOption(style: style, option: "Default")
cancel.waitForExistenceAndTap()
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Fail On Block (for modal/popover vc)")
chooseStyleAndOption(style: style, option: "Fail On Block (for modal/popover vc)")
XCTAssertTrue(initialScreenVisible)

showDismissablePresentation(style: style, option: "Unanimated")
chooseStyleAndOption(style: style, option: "Unanimated")
XCTAssertEqual(cancel.exists, false)
pressDismiss()
XCTAssertTrue(initialScreenVisible)
Expand All @@ -111,11 +111,11 @@ class ExampleUITests: XCTestCase {
let okButton = app.sheets.buttons["OK"]

verifyForAllContainerConfigurations {
showDismissablePresentation(style: style, option: "Fail On Block (for modal/popover vc)")
chooseStyleAndOption(style: style, option: "Fail On Block (for modal/popover vc)")
XCTAssertEqual(okButton.exists, false)

showDismissablePresentation(style: style, option: "Default")
okButton.tap()
chooseStyleAndOption(style: style, option: "Default")
okButton.waitForExistenceAndTap()
XCTAssertTrue(initialScreenVisible)
}
}
Expand All @@ -124,7 +124,7 @@ class ExampleUITests: XCTestCase {
let style = "embed"

verifyForAllContainerConfigurations {
showDismissablePresentation(style: style, option: "Default")
chooseStyleAndOption(style: style, option: "Default")
XCTAssertTrue(initialScreenVisible)

pressDismiss()
Expand All @@ -136,37 +136,76 @@ class ExampleUITests: XCTestCase {
let style = "invisible"

verifyForAllContainerConfigurations {
showDismissablePresentation(style: style, option: "Default")
chooseStyleAndOption(style: style, option: "Default")
XCTAssertTrue(initialScreenVisible)
}
}

// Issue: https://github.com/iZettle/Presentation/issues/36
func disabled_testNavigationBarVisibilityPreference() {
func testSwipeDownToDismissModal() {
if #available(iOS 13.0, *) {
let style = "modal"
let dismissButton = app.buttons["Tap To Dismiss"]
let navBar = app.navigationBars["UIView"]

func swipeDown(afterExistenseOf requiredElement: XCUIElement) {
XCTAssertTrue(requiredElement.waitForExistence(timeout: 1.0))
app.swipeDown()
}

func dragDownFromNavigationBar(to toElement: XCUIElement, afterExistenseOf requiredElement: XCUIElement) {
XCTAssertTrue(requiredElement.waitForExistence(timeout: 1.0))
navBar.press(forDuration: 0.5, thenDragTo: toElement)
}

verifyForAllContainerConfigurations {
chooseStyleAndOption(style: style, option: "Show alert on swipe down to dismiss")
swipeDown(afterExistenseOf: dismissButton)
pressAlertOK()
dismissButton.tap()

chooseStyleAndOption(style: style, option: "Embed in navigation and swipe down to dismiss")
swipeDown(afterExistenseOf: dismissButton)
pressAlertOK()
dismissButton.tap()

// Drag modal down and dismiss it
chooseStyleAndOption(style: style, option: "Default")
dragDownFromNavigationBar(to: dismissButton, afterExistenseOf: dismissButton)
XCTAssertFalse(dismissButton.exists)

// When in navigation stack with more than one view controller, dragging down dismisses a view only if that option has been passed
chooseStyleAndOption(style: style, option: "Allow swipe to dismiss always")
dragDownFromNavigationBar(to: dismissButton, afterExistenseOf: navBar.buttons["Back"])
XCTAssertFalse(dismissButton.exists)
}
}
}

func testNavigationBarVisibilityPreference() {
app.launch()
showDismissablePresentation(style: "default", option: "NavigationBar visibility preference")
chooseStyleAndOption(style: "default", option: "NavigationBar visibility preference")
let navBar = app.navigationBars["UIView"]
let nextButton = app.buttons["Next"]
let backButton = navBar.buttons["Back"]

XCTAssertTrue(navBar.exists)

nextButton.tap()
nextButton.waitForExistenceAndTap()
XCTAssertFalse(navBar.exists)

nextButton.tap()
nextButton.waitForExistenceAndTap()
XCTAssertTrue(navBar.exists)

backButton.tap()
backButton.waitForExistenceAndTap()
XCTAssertFalse(navBar.exists)

nextButton.tap()
nextButton.waitForExistenceAndTap()
XCTAssertTrue(navBar.exists)

nextButton.tap()
nextButton.waitForExistenceAndTap()
XCTAssertFalse(navBar.exists)

nextButton.tap()
nextButton.waitForExistenceAndTap()
XCTAssertFalse(navBar.exists)

app.terminate()
Expand All @@ -185,26 +224,38 @@ class ExampleUITests: XCTestCase {
}
}

func showDismissablePresentation(style: String, option: String) {
func chooseStyleAndOption(style: String, option: String, file: StaticString = #file, line: UInt = #line) {
let tablesQuery = app.tables

XCTAssertTrue(app.navigationBars["Presentation Styles"].exists)
XCTAssertTrue(app.navigationBars["Presentation Styles"].exists, file: file, line: line)
tablesQuery.cells.staticTexts[style].tap()
XCTAssertTrue(app.navigationBars["Presentation Options"].exists)
XCTAssertTrue(app.navigationBars["Presentation Options"].exists, file: file, line: line)
tablesQuery.cells.staticTexts[option].tap()
}

func pressBack() {
let back = app.navigationBars["UIView"].buttons.firstMatch
back.tap()
func pressAlertOK(file: StaticString = #file, line: UInt = #line) {
let okButton = app.alerts.buttons["OK"]
okButton.waitForExistenceAndTap(file: file, line: line)
}

func pressDismiss() {
func pressBack(file: StaticString = #file, line: UInt = #line) {
let back = app.navigationBars["UIView"].buttons.firstMatch
back.waitForExistenceAndTap(file: file, line: line)
}

func pressDismiss(file: StaticString = #file, line: UInt = #line) {
let dismiss = app.buttons["Tap To Dismiss"]
dismiss.tap()
dismiss.waitForExistenceAndTap(file: file, line: line)
}

var initialScreenVisible: Bool {
return app.navigationBars["Presentation Styles"].exists
}
}

extension XCUIElement {
func waitForExistenceAndTap(file: StaticString = #file, line: UInt = #line) {
XCTAssert(self.waitForExistence(timeout: 1.0), file: file, line: line)
self.tap()
}
}
Loading