Skip to content

Commit 9d7f21e

Browse files
committed
fix(ios): should fix bottomsheet and safearea
1 parent 015d5aa commit 9d7f21e

File tree

1 file changed

+239
-76
lines changed

1 file changed

+239
-76
lines changed

src/bottomsheet/bottomsheet.ios.ts

Lines changed: 239 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { layout } from '@nativescript/core/utils/utils';
55
import { BottomSheetOptions } from './bottomsheet';
66
import { fromObject } from '@nativescript/core/data/observable';
77
import { applyMixins } from 'nativescript-material-core/core';
8+
import { ios as iosUtils } from '@nativescript/core/utils/utils';
9+
import { ios as iosView } from '@nativescript/core/ui/core/view/view-helper';
10+
import { Page } from '@nativescript/core/ui/page';
811

12+
const majorVersion = iosUtils.MajorVersion;
913
class MDCBottomSheetControllerDelegateImpl extends NSObject implements MDCBottomSheetControllerDelegate {
1014
public static ObjCProtocols = [MDCBottomSheetControllerDelegate];
1115

@@ -50,101 +54,241 @@ declare module '@nativescript/core/ui/core/view/view' {
5054
}
5155
}
5256
}
57+
declare module '@nativescript/core/ui/core/view-base' {
58+
interface ViewBase {
59+
_layoutParent();
60+
}
61+
}
62+
63+
function initLayoutGuide(controller: UIViewController) {
64+
const rootView = controller.view;
65+
const layoutGuide = UILayoutGuide.alloc().init();
66+
rootView.addLayoutGuide(layoutGuide);
67+
NSLayoutConstraint.activateConstraints(<any>[
68+
layoutGuide.topAnchor.constraintEqualToAnchor(controller.topLayoutGuide.bottomAnchor),
69+
layoutGuide.bottomAnchor.constraintEqualToAnchor(controller.bottomLayoutGuide.topAnchor),
70+
layoutGuide.leadingAnchor.constraintEqualToAnchor(rootView.leadingAnchor),
71+
layoutGuide.trailingAnchor.constraintEqualToAnchor(rootView.trailingAnchor)
72+
]);
73+
74+
return layoutGuide;
75+
}
76+
function layoutView(controller: UILayoutViewController, owner: View): void {
77+
let layoutGuide = controller.view.safeAreaLayoutGuide;
78+
if (!layoutGuide) {
79+
traceWrite(`safeAreaLayoutGuide during layout of ${owner}. Creating fallback constraints, but layout might be wrong.`, traceCategories.Layout, traceMessageType.error);
80+
81+
layoutGuide = initLayoutGuide(controller);
82+
}
83+
const safeArea = layoutGuide.layoutFrame;
84+
let position = ios.getPositionFromFrame(safeArea);
85+
const safeAreaSize = safeArea.size;
86+
87+
const hasChildViewControllers = controller.childViewControllers.count > 0;
88+
if (hasChildViewControllers) {
89+
const fullscreen = controller.view.frame;
90+
position = ios.getPositionFromFrame(fullscreen);
91+
}
92+
93+
const safeAreaWidth = layout.round(layout.toDevicePixels(safeAreaSize.width));
94+
const safeAreaHeight = layout.round(layout.toDevicePixels(safeAreaSize.height));
95+
96+
const widthSpec = layout.makeMeasureSpec(safeAreaWidth, layout.EXACTLY);
97+
const heightSpec = layout.makeMeasureSpec(safeAreaHeight, layout.UNSPECIFIED);
98+
99+
View.measureChild(null, owner, widthSpec, heightSpec);
100+
const marginTop = owner.effectiveMarginTop;
101+
const marginBottom = owner.effectiveMarginBottom;
102+
const marginLeft = owner.effectiveMarginLeft + position.left;
103+
const marginRight = owner.effectiveMarginRight;
104+
let top = marginTop + position.top;
105+
const width = owner.getMeasuredWidth();
106+
let height = owner.getMeasuredHeight();
107+
108+
owner.iosOverflowSafeArea = false;
109+
110+
View.layoutChild(null, owner, position.left, position.top, position.left + width, position.top + height);
111+
112+
const effectiveWidth = width + marginLeft + marginRight;
113+
let effectiveHeight = height + top + marginBottom;
114+
if (controller.ignoreTopSafeArea || controller.ignoreBottomSafeArea) {
115+
const frame = owner.nativeViewProtected.frame;
116+
const availableSpace = getAvailableSpaceFromParent(owner, frame);
117+
// const safeArea = availableSpace.safeArea;
118+
// const fullscreen = availableSpace.fullscreen;
119+
// const inWindow = availableSpace.inWindow;
120+
121+
const position = ios.getPositionFromFrame(frame);
122+
const fullscreenPosition = ios.getPositionFromFrame(availableSpace.fullscreen);
123+
const safeAreaPosition = ios.getPositionFromFrame(availableSpace.safeArea);
124+
125+
const adjustedPosition = position;
126+
127+
if (controller.ignoreTopSafeArea) {
128+
const delta = safeAreaPosition.top - fullscreenPosition.top;
129+
effectiveHeight -= delta;
130+
adjustedPosition.bottom -= delta;
131+
adjustedPosition.top -= delta;
132+
}
133+
if (controller.ignoreBottomSafeArea) {
134+
const delta = fullscreenPosition.bottom - safeAreaPosition.bottom ;
135+
effectiveHeight -= delta;
136+
// adjustedPosition.bottom += delta * 2;
137+
}
138+
owner.nativeViewProtected.frame = CGRectMake(
139+
layout.toDeviceIndependentPixels(adjustedPosition.left),
140+
layout.toDeviceIndependentPixels(adjustedPosition.top),
141+
layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left),
142+
layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top)
143+
);
144+
}
145+
controller.preferredContentSize = CGSizeMake(layout.toDeviceIndependentPixels(effectiveWidth), layout.toDeviceIndependentPixels(effectiveHeight));
146+
147+
if (owner.parent) {
148+
owner.parent._layoutParent();
149+
}
150+
}
151+
function getAvailableSpaceFromParent(view: View, frame: CGRect): { safeArea: CGRect; fullscreen: CGRect; inWindow: CGRect } {
152+
if (!view) {
153+
return null;
154+
}
155+
156+
let scrollView = null;
157+
let viewControllerView = null;
53158

54-
class BottomSheetUILayoutViewController extends UIViewController {
159+
if (view.viewController) {
160+
viewControllerView = view.viewController.view;
161+
} else {
162+
let parent = view.parent as View;
163+
while (parent && !parent.viewController && !(parent.nativeViewProtected instanceof UIScrollView)) {
164+
parent = parent.parent as View;
165+
}
166+
167+
if (parent.nativeViewProtected instanceof UIScrollView) {
168+
scrollView = parent.nativeViewProtected;
169+
} else if (parent.viewController) {
170+
viewControllerView = parent.viewController.view;
171+
}
172+
}
173+
174+
let fullscreen = null;
175+
let safeArea = null;
176+
177+
if (viewControllerView) {
178+
safeArea = viewControllerView.safeAreaLayoutGuide.layoutFrame;
179+
fullscreen = viewControllerView.frame;
180+
} else if (scrollView) {
181+
const insets = scrollView.safeAreaInsets;
182+
safeArea = CGRectMake(insets.left, insets.top, scrollView.contentSize.width - insets.left - insets.right, scrollView.contentSize.height - insets.top - insets.bottom);
183+
fullscreen = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
184+
}
185+
186+
const locationInWindow = view.getLocationInWindow();
187+
let inWindowLeft = locationInWindow.x;
188+
let inWindowTop = locationInWindow.y;
189+
190+
if (scrollView) {
191+
inWindowLeft += scrollView.contentOffset.x;
192+
inWindowTop += scrollView.contentOffset.y;
193+
}
194+
195+
const inWindow = CGRectMake(inWindowLeft, inWindowTop, frame.size.width, frame.size.height);
196+
197+
return { safeArea: safeArea, fullscreen: fullscreen, inWindow: inWindow };
198+
}
199+
200+
class UILayoutViewController extends UIViewController {
55201
public owner: WeakRef<View>;
56202
ignoreBottomSafeArea: boolean;
57203
ignoreTopSafeArea: boolean;
58204

59-
public static initWithOwner(owner: WeakRef<View>): BottomSheetUILayoutViewController {
60-
const controller = <BottomSheetUILayoutViewController>BottomSheetUILayoutViewController.new();
205+
public static initWithOwner(owner: WeakRef<View>): UILayoutViewController {
206+
const controller = <UILayoutViewController>UILayoutViewController.new();
61207
controller.owner = owner;
62-
controller.ignoreBottomSafeArea = false;
63-
controller.ignoreTopSafeArea = true;
208+
64209
return controller;
65210
}
66211

67-
public viewDidLayoutSubviews(): void {
68-
super.viewDidLayoutSubviews();
212+
public viewDidLoad(): void {
213+
super.viewDidLoad();
214+
215+
// Unify translucent and opaque bars layout
216+
// this.edgesForExtendedLayout = UIRectEdgeBottom;
217+
this.extendedLayoutIncludesOpaqueBars = true;
218+
}
219+
220+
public viewWillLayoutSubviews(): void {
221+
super.viewWillLayoutSubviews();
69222
const owner = this.owner.get();
70223
if (owner) {
71-
this.layoutView(this, owner);
224+
iosView.updateConstraints(this, owner);
72225
}
73226
}
74227

75-
public viewWillAppear(animated: boolean): void {
76-
super.viewWillAppear(animated);
228+
public viewDidLayoutSubviews(): void {
229+
super.viewDidLayoutSubviews();
77230
const owner = this.owner.get();
78-
if (!owner) {
79-
return;
80-
}
231+
if (owner) {
232+
if (majorVersion >= 11) {
233+
// Handle nested UILayoutViewController safe area application.
234+
// Currently, UILayoutViewController can be nested only in a TabView.
235+
// The TabView itself is handled by the OS, so we check the TabView's parent (usually a Page, but can be a Layout).
236+
const tabViewItem = owner.parent;
237+
const tabView = tabViewItem && tabViewItem.parent;
238+
let parent = tabView && tabView.parent;
239+
240+
// Handle Angular scenario where TabView is in a ProxyViewContainer
241+
// It is possible to wrap components in ProxyViewContainers indefinitely
242+
// Not using instanceof ProxyViewContainer to avoid circular dependency
243+
// TODO: Try moving UILayoutViewController out of view module
244+
while (parent && !parent.nativeViewProtected) {
245+
parent = parent.parent;
246+
}
247+
const additionalInsets = { top: 0, left: 0, bottom: 0, right: 0 };
81248

82-
ios.updateAutoAdjustScrollInsets(this, owner);
249+
if (parent) {
250+
const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top;
251+
const currentInsetsTop = this.view.safeAreaInsets.top;
252+
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
83253

84-
if (!owner.parent) {
85-
owner.callLoaded();
86-
}
87-
}
254+
const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom;
255+
const currentInsetsBottom = this.view.safeAreaInsets.bottom;
256+
const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0);
88257

89-
layoutView(controller: UIViewController, owner: View): void {
90-
// the safe area of the controller is not correct. I think materialcomponents ios is changing the safeArea of the controller
91-
// let s look at the app root controller to get fulllscreen safe area
92-
let layoutGuide = controller.view.safeAreaLayoutGuide;
93-
const fullscreen = controller.view.frame;
94-
const safeArea = layoutGuide.layoutFrame;
95-
let safeAreaPosition = ios.getPositionFromFrame(safeArea);
96-
const safeAreaSize = safeArea.size;
258+
if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) {
259+
additionalInsets.top = additionalInsetsTop;
260+
additionalInsets.bottom = additionalInsetsBottom;
261+
}
262+
}
263+
// if (this.ignoreTopSafeArea === true) {
264+
// console.log('ignoreTopSafeArea', additionalInsets.top, this.view.safeAreaLayoutGuide.layoutFrame.origin.x, this.view.safeAreaInsets.top);
265+
// additionalInsets.top += this.view.safeAreaLayoutGuide.layoutFrame.origin.x;
266+
// }
97267

98-
const hasChildViewControllers = controller.childViewControllers.count > 0;
99-
if (hasChildViewControllers) {
100-
safeAreaPosition = ios.getPositionFromFrame(fullscreen);
101-
}
268+
// if (this.ignoreBottomSafeArea === true) {
269+
// additionalInsets.bottom -= this.view.safeAreaInsets.bottom;
270+
// }
102271

103-
const safeAreaWidth = layout.round(layout.toDevicePixels(safeAreaSize.width));
104-
const safeAreaHeight = layout.round(layout.toDevicePixels(safeAreaSize.height));
105-
106-
const widthSpec = layout.makeMeasureSpec(safeAreaWidth, layout.EXACTLY);
107-
const heightSpec = layout.makeMeasureSpec(safeAreaHeight, layout.UNSPECIFIED);
108-
109-
View.measureChild(null, owner, widthSpec, heightSpec);
110-
const marginTop = owner.effectiveMarginTop;
111-
const marginBottom = owner.effectiveMarginBottom;
112-
const marginLeft = owner.effectiveMarginLeft + safeAreaPosition.left;
113-
const marginRight = owner.effectiveMarginRight;
114-
let top = marginTop;
115-
const width = owner.getMeasuredWidth();
116-
let height = owner.getMeasuredHeight();
117-
if (!this.ignoreTopSafeArea) {
118-
top += safeAreaPosition.top;
119-
}
120-
const effectiveWidth = width + marginLeft + marginRight;
121-
let effectiveHeight = height + top + marginBottom;
122-
if (this.ignoreBottomSafeArea) {
123-
effectiveHeight -= ios.getPositionFromFrame(fullscreen).bottom - safeAreaPosition.bottom;
124-
}
125-
View.layoutChild(null, owner, marginLeft, top, width + marginLeft, height + top);
126-
this.preferredContentSize = CGSizeMake(layout.toDeviceIndependentPixels(effectiveWidth), layout.toDeviceIndependentPixels(effectiveHeight));
272+
const insets = new UIEdgeInsets(additionalInsets);
273+
this.additionalSafeAreaInsets = insets;
274+
}
127275

128-
this.layoutParent(owner.parent);
276+
layoutView(this, owner);
277+
}
129278
}
130279

131-
layoutParent(view: ViewBase): void {
132-
if (!view) {
280+
public viewWillAppear(animated: boolean): void {
281+
super.viewWillAppear(animated);
282+
const owner = this.owner.get();
283+
if (!owner) {
133284
return;
134285
}
135286

136-
if (view instanceof View && view.nativeViewProtected) {
137-
const frame = view.nativeViewProtected.frame;
138-
const origin = frame.origin;
139-
const size = frame.size;
140-
const left = layout.toDevicePixels(origin.x);
141-
const top = layout.toDevicePixels(origin.y);
142-
const width = layout.toDevicePixels(size.width);
143-
const height = layout.toDevicePixels(size.height);
144-
view._setLayoutFlags(left, top, width + left, height + top);
145-
}
287+
iosView.updateAutoAdjustScrollInsets(this, owner);
146288

147-
this.layoutParent(view.parent);
289+
if (!owner.parent) {
290+
owner.callLoaded();
291+
}
148292
}
149293

150294
public viewDidDisappear(animated: boolean): void {
@@ -154,6 +298,22 @@ class BottomSheetUILayoutViewController extends UIViewController {
154298
owner.callUnloaded();
155299
}
156300
}
301+
302+
// Mind implementation for other controllers
303+
public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void {
304+
super.traitCollectionDidChange(previousTraitCollection);
305+
306+
if (majorVersion >= 13) {
307+
const owner = this.owner.get();
308+
if (
309+
owner &&
310+
this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection &&
311+
this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)
312+
) {
313+
owner.notify({ eventName: 'traitCollectionColorAppearanceChanged', object: owner });
314+
}
315+
}
316+
}
157317
}
158318

159319
export class ViewWithBottomSheet extends ViewWithBottomSheetBase {
@@ -177,19 +337,23 @@ export class ViewWithBottomSheet extends ViewWithBottomSheetBase {
177337
traceWrite('Parent page is not part of the window hierarchy.', traceCategories.ViewHierarchy, traceMessageType.error);
178338
return;
179339
}
180-
181340
this._setupAsRootView({});
182341

183342
this._commonShowNativeBottomSheet(parentWithController, options);
184-
let controller: BottomSheetUILayoutViewController = this.viewController;
343+
let controller: UILayoutViewController = this.viewController;
185344
if (!controller) {
186345
const nativeView = this.ios || this.nativeViewProtected;
187-
controller = BottomSheetUILayoutViewController.initWithOwner(new WeakRef(this));
346+
controller = UILayoutViewController.initWithOwner(new WeakRef(this));
347+
// newController = iosView.UILayoutViewController.initWithOwner(new WeakRef(item.content)) as UIViewController;
188348
if (options.ignoreBottomSafeArea !== undefined) {
189349
controller.ignoreBottomSafeArea = options.ignoreBottomSafeArea;
350+
} else {
351+
controller.ignoreBottomSafeArea = false;
190352
}
191353
if (options.ignoreTopSafeArea !== undefined) {
192354
controller.ignoreTopSafeArea = options.ignoreTopSafeArea;
355+
} else {
356+
controller.ignoreTopSafeArea = true;
193357
}
194358
if (nativeView instanceof UIView) {
195359
controller.view.addSubview(nativeView);
@@ -220,12 +384,11 @@ export class ViewWithBottomSheet extends ViewWithBottomSheetBase {
220384
(<any>controller).animated = true;
221385
parentController.presentViewControllerAnimatedCompletion(bottomSheet, true, null);
222386
if (options.transparent === true) {
223-
bottomSheet.view.backgroundColor = UIColor.clearColor;
387+
controller.view.backgroundColor = UIColor.clearColor;
224388
// for it to be more beautiful let s disable elevation
225-
bottomSheet.view['elevation'] = 0;
226-
} else {
227-
// this.backgroundColor = 'white';
228-
bottomSheet.view.backgroundColor = UIColor.whiteColor;
389+
controller.view['elevation'] = 0;
390+
} else if (!(this instanceof Page)) {
391+
controller.view.backgroundColor = majorVersion <= 12 && !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor;
229392
}
230393
const transitionCoordinator = bottomSheet.transitionCoordinator;
231394
if (transitionCoordinator) {

0 commit comments

Comments
 (0)