Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

[iOS] Pushing a modal on application start should not show MainPage #432

Closed
wants to merge 8 commits into from
Closed

[iOS] Pushing a modal on application start should not show MainPage #432

wants to merge 8 commits into from

Conversation

adrianknight89
Copy link
Contributor

Description of Change

If you push a modal page on application start, iOS still shows MainPage briefly. This is not ideal in cases where a login flow should be shown and removed modally. Ideally, we should avoid re-setting MainPage to change navigation stacks. This PR will let you add an overlay to any page when pushing modally by using a platform specific variable. It also addresses some architectural problems.

Due to the nature of async, PageOverlayColorWhenPushingModal should be disabled inside the appearing handler of the modal page. It can also be used in cases where pushing a modal without the animation doesn't look right - not just on application start but throughout the application. Simply set the right overlay color before pushing modally. On start, this color is usually the background color of the launch image or the modal page.

Overlays are pushed on the platform renderer view so they should cover any system bars such as the navigation bar.

Bugs Fixed

API Changes

Added:

  • internal bool Appeared { get; private set; } in ModalWrapper
  • public static Color? GetPageOverlayColorWhenPushingModal(BindableObject element)
  • public static void SetPageOverlayColorWhenPushingModal(BindableObject element, Color? value)
  • public static Color? PageOverlayColorWhenPushingModal(this IPlatformElementConfiguration<iOS, FormsElement> config)
  • public static IPlatformElementConfiguration<iOS, FormsElement> SetPageOverlayColorWhenPushingModal(this IPlatformElementConfiguration<iOS, FormsElement> config, Color? value)
  • public static IPlatformElementConfiguration<iOS, FormsElement> EnablePageOverlayColorWhenPushingModal(this IPlatformElementConfiguration<iOS, FormsElement> config)
  • public static IPlatformElementConfiguration<iOS, FormsElement> DisablePageOverlayColorWhenPushingModal(this IPlatformElementConfiguration<iOS, FormsElement> config)

PR Checklist

  • Has tests (if omitted, state reason in description)
  • Rebased on top of master at time of PR
  • Changes adhere to coding standard
  • Consolidate commits as makes sense

while (!wrapper.Appeared)
{
await Task.Delay(5);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://developer.apple.com/reference/uikit/uiviewcontroller/1621380-presentviewcontroller

The completion handler is called after the viewDidAppear: method is called on the presented view controller.

Looks like PresentViewControllerAsync is a Xamarin wrapper around the iOS method. The async is probably returning after PresentViewController returns but not after the completion callback is raised. While Task.Delay(5); might work in most cases, it didn't look right to me. I created a read-only Appeared property in the modal wrapper so that we complete our async push only when the view has actually appeared.

@@ -433,6 +432,12 @@ bool PageIsChildOfPlatform(Page page)

async Task PresentModal(Page modal, bool animated)
{
AddPageOverlay();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add and remove page overlay during modal present.

if (_appeared)
return PresentModal(modal, _animateModals && animated);
return Task.FromResult<object>(null);
return PresentModal(modal, _animateModals && animated);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

PushModalAsync should not return a Task with no result. This is going to abort push in cases where _appeared is false. We'll just remove the check and call PresentModal right away.

while (!_appeared)
{
await Task.Delay(5);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you want to avoid

Warning: Attempt to present <Xamarin_Forms_Platform_iOS_ModalWrapper: 0x1647b8c0> on <Xamarin_Forms_Platform_iOS_PlatformRenderer: 0x16117c80> whose view is not in the window hierarchy!

then the modal should be pushed only after the presenting controller has appeared. We'll resort to Delay till the controller has raised ViewDidAppear.


rootRenderer.SetElementSize(new Size(_renderer.View.Bounds.Width, _renderer.View.Bounds.Height));
if (_pageOverlayView != null && !ReferenceEquals(_pageOverlayView, _renderer.View.Subviews.LastOrDefault()))
throw new InvalidOperationException("Overlay must be the topmost subview of MainPage.");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should ensure any future code changes to LayoutSubviews do not add a subview atop the overlay.

base.ViewDidAppear(animated);
Platform.DidAppear();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Platform.DidAppear isn't doing anything magical to require to be called before the base call. In most cases, base should appear first.


namespace Xamarin.Forms.Platform.iOS
{
internal class ModalWrapper : UIViewController
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This can be moved to its own class file like NativeViewWrapper.

DisposeModelAndChildrenRenderers(modal);
}

DisposeModelAndChildrenRenderers(Page);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Modals should be disposed before Page.

View.BackgroundColor = UIColor.White;
Platform.WillAppear();
base.ViewWillAppear(animated);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code isn't doing anything magical either. If you look at Platform.WillAppear, it only executes once due to _appeared (which never gets set to false once true). Setting View.BackgroundColor of the platform renderer can be done in Platform while it's being initialized. We're not changing View.BackgroundColor anywhere to justify setting it to white again.


Page.DescendantRemoved += HandleChildRemoved;

_appeared = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All of this code can be moved to Platform constructor and SetPage. Note that SetPage never executed below _appeared = false because when it's called (there is only one reference to SetPage), the flag is always false.

BackgroundColor = UIColor.White,
ContentMode = UIViewContentMode.Redraw
}
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Left from changes to ViewWillAppear. These can be set once during initialization.

@hartez
Copy link
Contributor

hartez commented Jan 25, 2017

@adrianknight89 See my comment on 29382 - I believe this problem may have been addressed elsewhere. Can you still reproduce the problem with a more recent version of XF?

@hartez hartez self-assigned this Jan 25, 2017
@adrianknight89
Copy link
Contributor Author

@hartez I tested your updated repro and wasn't able to reproduce the issue.

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

Successfully merging this pull request may close these issues.

4 participants