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

iOS 13 modal sheets (issue #41) #43

merged 3 commits into from
Jul 26, 2019

Conversation

nataliq
Copy link
Contributor

@nataliq nataliq commented Jul 17, 2019

What's new:
On iOS 13 we have new default modal presentation called Sheet. The modal Sheets can be dismissed by swiping them down, and in doing so, Presentation framework wasn't disposing the bag of its presentation.
This PR makes Presentation's view controller to conform to UIAdaptivePresentationDelegate that will handle actions for when the view attempt's, will and did dismiss and disposing the bag appropriately when the view dismisses.

In addition, it adds functionality for the developer to pass a block of code when the view attempts to dismiss through a signal in CustomAdaptivePresentationDelegate like this

vc.customAdaptivePresentationDelegate.didAttemptToDismissSignal.onValue { ... }

The didAttemptToDismissSignal will fire only if the view controller that's presented has .isModalInPresentation = true and then the developer can execute a block with what they want to happen when the user attempt to dismiss (optional)

This PR also includes a temp solution for #40

More info: #41

Copy link
Contributor

@niil-ohlin niil-ohlin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. When I was looking into this issue previously I thought about doing the same solution but did not get as far! Nice work!

@iZNunev
Copy link
Contributor

iZNunev commented Jul 19, 2019

I've added a new file to presentation and it the test-iOS on Circle-CI is failing due to

Build input file cannot be found: '/System/Volumes/Data/Users/vasil-nunev/Presentation/Presentation/CustomAdaptivePresentationDelegate.swift' (in target 'Presentation')

Anyone knows how to fix this?

@iZNunev iZNunev marked this pull request as ready for review July 22, 2019 07:59
@iZNunev iZNunev requested a review from niil-ohlin July 22, 2019 08:00
@nataliq nataliq changed the title [WIP] iOS 13 modal sheets (issue #41) iOS 13 modal sheets (issue #41) Jul 22, 2019
}
}

private var customAdaptivePresentationDelegateKey = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this property be modified?
If not can it be made a let?

moglistree
moglistree previously approved these changes Jul 22, 2019
Copy link
Contributor

@moglistree moglistree left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems nice.
Is it possible to squash some of the progress commits?

Presentation/CustomAdaptivePresentationDelegate.swift Outdated Show resolved Hide resolved
get { return associatedValue(forKey: &customAdaptivePresentationDelegateKey, initial: CustomAdaptivePresentationDelegate()) }
set { setAssociatedValue(newValue, forKey: &customAdaptivePresentationDelegateKey) }
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we want to expose this like this?

I like this kind of pattern:

extension AVCaptureDataOutputSynchronizer {
    var dataCollectionSignal: Signal<AVCaptureSynchronizedDataCollection> {
        class Delegate: NSObject, AVCaptureDataOutputSynchronizerDelegate {
            let callbacker = Callbacker<AVCaptureSynchronizedDataCollection>()
            func dataOutputSynchronizer(_ synchronizer: AVCaptureDataOutputSynchronizer, didOutput synchronizedDataCollection: AVCaptureSynchronizedDataCollection) {
                    callbacker.callAll(with: synchronizedDataCollection)
            }
        }
        return Signal { callback in
            let bag = DisposeBag()
            let delegate = (self.delegate as? Delegate) ?? Delegate()
            bag.hold(delegate)
            bag.hold(self)
            self.setDelegate(delegate, queue: .outputSynchronizer)
            bag += delegate.callbacker.addCallback(callback)

            return bag
        }
    }
}

So instead of exposing CustomAdaptivePresentationDelegate and customAdaptivePresentationDelegate you just expose didAttemptToDismissSignal on UIViewController.

Presentation/PresentationStyle.swift Outdated Show resolved Hide resolved
@@ -274,3 +289,82 @@ private class PopoverPresentationControllerDelegate: NSObject, UIPopoverPresenta
}

private var isIpad: Bool { return UIDevice.current.userInterfaceIdiom == .pad }

private class AdaptiveProxyPresentationDelegate: NSObject, UIAdaptivePresentationControllerDelegate {
private let didDismissCallbacker = Callbacker<()>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some more reading of the code and hopefully a better understanding I wonder if it is not cleaner to make AdaptiveProxyPresentationDelegate public with support for all the delegates signals and delegates similar to tableViewDelegate and friends. Then we can check when modally presenting if the delegate is nil or of type AdaptiveProxyPresentationDelegate and use the delegate to handle dismiss, or else not handle dismiss (as someone else is doing that instead) and potentially log a warning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mansbernhardt we did some changes based on this feedback. Unfortunately, we can't allow the user's to set directly the delegate to the presentationController of the vc because it causes memory leak problems when trying to access the presentationController before it gets presented on the screen. Instead we keep the custom delegate on the VC and the user needs to set that one instead so that Presentation can set it properly when we are presenting the vc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow? Who is leaking? The one who is setting the delegate needs to hold on to the delegate as well using e.g. bag.hold(delegate)

/// will be able to be swiped down.
///
/// By default on an navigation stack only the first view is allowed to be dismissed by swiping down.
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be handled else where by using a Delegate<(), Bool> instead?

And the below including allowSwipeDismissAlways could be moved else-where?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved.

@@ -274,3 +289,82 @@ private class PopoverPresentationControllerDelegate: NSObject, UIPopoverPresenta
}

private var isIpad: Bool { return UIDevice.current.userInterfaceIdiom == .pad }

private class AdaptiveProxyPresentationDelegate: NSObject, UIAdaptivePresentationControllerDelegate {
private let didDismissCallbacker = Callbacker<()>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow? Who is leaking? The one who is setting the delegate needs to hold on to the delegate as well using e.g. bag.hold(delegate)

}

customPresentationDelegate.allowSwipeDismissAlways = options.contains(.allowSwipeDismissAlways)
vc.presentationController?.delegate = customPresentationDelegate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the memory leak issue. I was thinking something like this instead:

let delegate =  (vc.presentationController?.delegate as? CustomAdaptivePresentationDelegate) ?? CustomAdaptivePresentationDelegate()
vc.presentationController?.delegate = delegate
bag.hold(delegate)
bag += delagate.presentationControllerShouldDismiss.set {
  return Bool.... based on .allowSwipeDismissAlways etc
}

bag += delagate.didDismissSignal.onValue {
  completion(.failure(PresentError.dismissed))
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very weird leak, will try to understand more about it now. Basically seeing a memory leak alert (and a check in the memory graph confirms it) when running the UI tests when accessing viewController.presentationController (even when the rest of the logic is commented out). I think the problem is coming from the fact that If you have not yet presented the current view controller, accessing presentationController creates a presentation controller. In our case viewController is not presented but if vc and viewController are different (when embedding in a navigation controller for example) then the one that will be presented and needs a presentation controller is vc and the presentation controller we just created for viewController leaks. Maybe related to http://www.openradar.appspot.com/36256721 . Will test this assumption now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's what's happening. Here is a branch where you can reproduce it: https://github.com/iZettle/Presentation/tree/ios13-modal-sheets-leak. Not sure if there is a better workaround for a leak in UIKit. We can ofc add a comment about it...

Screenshot 2019-07-25 at 12 42 08

import UIKit
import Flow

public final class CustomAdaptivePresentationDelegate: NSObject, UIAdaptivePresentationControllerDelegate {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this to be available for < iOS 13.0?

Should we move the conformance to UIAdaptivePresentationControllerDelegate to the extension where we implement it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protocol has few iOS 8 methods, added them too. Prefer to keep the iOS 13 only signals here as well, they won't signal before iOS 13 but no need of a check on the call side too.

mansbernhardt
mansbernhardt previously approved these changes Jul 26, 2019
Copy link
Contributor

@mansbernhardt mansbernhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition!

public var didDismissSignal: Signal<UIPresentationController> {
return Signal(callbacker: didDismissCallbacker)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these three signals be exposed for < iOS 13 users? Will they ever signal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's nicer than adding iOS 13 check everywhere they are used. I specified it in the docs.

}

public extension UIViewController {
/// A custom delegate that allows you to listen to a Signals for adaptive presentations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar and spacing.

if !(vc is UIAlertController) {
let customPresentationDelegate: CustomAdaptivePresentationDelegate
if let givenDelegate = viewController.customAdaptivePresentationDelegate {
customPresentationDelegate = givenDelegate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rely on someone else holding a reference for us? If not, potential simplification:

let delegate = viewController.customAdaptivePresentationDelegate ?? CustomAdaptivePresentationDelegate()
bag.hold(delegate)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants