diff --git a/iOS/src/toga_iOS/constraints.py b/iOS/src/toga_iOS/constraints.py index dbf9ddcace..01662d336d 100644 --- a/iOS/src/toga_iOS/constraints.py +++ b/iOS/src/toga_iOS/constraints.py @@ -109,6 +109,7 @@ def update(self, x, y, width, height): # f"UPDATE CONSTRAINTS {self.widget} in {self.container} " # f"{width}x{height}@{x},{y}" # ) + y += self.container.top_offset self.left_constraint.constant = x self.top_constraint.constant = y diff --git a/iOS/src/toga_iOS/container.py b/iOS/src/toga_iOS/container.py index b67fad44fc..884efce188 100644 --- a/iOS/src/toga_iOS/container.py +++ b/iOS/src/toga_iOS/container.py @@ -1,3 +1,7 @@ +from rubicon.objc import objc_method, objc_property, send_super + +import toga + from .libs import ( UIApplication, UINavigationController, @@ -6,6 +10,9 @@ UIViewController, ) +toga.Widget.DEBUG_LAYOUT_ENABLED = True + + ####################################################################################### # Implementation notes: # @@ -15,8 +22,19 @@ ####################################################################################### +class TogaContainerView(UIView): + container = objc_property(object, weak=True) + + @objc_method + def safeAreaInsetsDidChange(self): + send_super(__class__, self, "safeAreaInsetsDidChange") + if self.container.content: + self.container.content.interface.refresh() + self.container.refreshed() + + class BaseContainer: - def __init__(self, content=None, on_refresh=None): + def __init__(self, content=None, on_refresh=None, safe_bottom=False): """A base class for iOS containers. :param content: The widget impl that is the container's initial content. @@ -25,6 +43,7 @@ def __init__(self, content=None, on_refresh=None): """ self._content = content self.on_refresh = on_refresh + self._safe_bottom = safe_bottom @property def content(self): @@ -52,7 +71,9 @@ def refreshed(self): class Container(BaseContainer): - def __init__(self, content=None, layout_native=None, on_refresh=None): + def __init__( + self, content=None, layout_native=None, on_refresh=None, safe_bottom=False + ): """ :param content: The widget impl that is the container's initial content. :param layout_native: The native widget that should be used to provide size @@ -62,9 +83,14 @@ def __init__(self, content=None, layout_native=None, on_refresh=None): the size can be different. :param on_refresh: The callback to be notified when this container's layout is refreshed. + :param safe_bottom: Whether the container should not extend into bottom + safe area insets. """ - super().__init__(content=content, on_refresh=on_refresh) - self.native = UIView.alloc().init() + super().__init__( + content=content, on_refresh=on_refresh, safe_bottom=safe_bottom + ) + self.native = TogaContainerView.alloc().init() + self.native.container = self self.native.translatesAutoresizingMaskIntoConstraints = True self.native.autoresizingMask = ( UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight @@ -83,7 +109,14 @@ def width(self): @property def height(self): - return self.layout_native.bounds.size.height - self.top_offset + if self._safe_bottom: + return ( + self.layout_native.bounds.size.height + - self.top_offset + - self.layout_native.safeAreaInsets.bottom + ) + else: + return self.layout_native.bounds.size.height - self.top_offset @property def top_offset(self): @@ -96,6 +129,7 @@ def __init__( content=None, layout_native=None, on_refresh=None, + safe_bottom=False, ): """ :param content: The widget impl that is the container's initial content. @@ -106,11 +140,14 @@ def __init__( rendered, the source of the size can be different. :param on_refresh: The callback to be notified when this container's layout is refreshed. + :param safe_bottom: Whether the container should not extend into bottom + safe area insets. """ super().__init__( content=content, layout_native=layout_native, on_refresh=on_refresh, + safe_bottom=safe_bottom, ) # Construct a ViewController that provides a navigation bar, and @@ -128,6 +165,7 @@ def __init__( content=None, layout_native=None, on_refresh=None, + safe_bottom=False, ): """A bare content container. @@ -141,11 +179,14 @@ def __init__( rendered, the source of the size can be different. :param on_refresh: The callback to be notified when this container's layout is refreshed. + :param safe_bottom: Whether the container should not extend into bottom + safe area insets. """ super().__init__( content=content, layout_native=layout_native, on_refresh=on_refresh, + safe_bottom=safe_bottom, ) # Construct a UIViewController to hold the root content @@ -174,6 +215,7 @@ def __init__( content=None, layout_native=None, on_refresh=None, + safe_bottom=False, ): """A top level container that provides a navigation/title bar. @@ -190,6 +232,7 @@ def __init__( content=content, layout_native=layout_native, on_refresh=on_refresh, + safe_bottom=safe_bottom, ) # Construct a NavigationController that provides a navigation bar, and diff --git a/iOS/src/toga_iOS/widgets/base.py b/iOS/src/toga_iOS/widgets/base.py index 48b852cc22..4d5e7d1a17 100644 --- a/iOS/src/toga_iOS/widgets/base.py +++ b/iOS/src/toga_iOS/widgets/base.py @@ -79,7 +79,7 @@ def set_tab_index(self, tab_index): def set_bounds(self, x, y, width, height): # print("SET BOUNDS", self, x, y, width, height, self.container.top_offset) - self.constraints.update(x, y + self.container.top_offset, width, height) + self.constraints.update(x, y, width, height) def set_text_align(self, alignment): pass diff --git a/iOS/src/toga_iOS/widgets/optioncontainer.py b/iOS/src/toga_iOS/widgets/optioncontainer.py index 783c501225..5f6b5f7197 100644 --- a/iOS/src/toga_iOS/widgets/optioncontainer.py +++ b/iOS/src/toga_iOS/widgets/optioncontainer.py @@ -1,3 +1,5 @@ +import platform + from rubicon.objc import SEL, objc_method, objc_property from travertino.size import at_least @@ -53,8 +55,9 @@ def create(self): self.native_controller.impl = self self.native_controller.delegate = self.native_controller - # Make the tab bar non-translucent, so you can actually see it. - self.native_controller.tabBar.setTranslucent(False) + if int(platform.release().split(".")[0]) < 26: # pragma: no branch + # Make the tab bar non-translucent, so you can actually see it. + self.native_controller.tabBar.setTranslucent(False) # The native widget representing the container is the view of the native # controller. This doesn't change once it's created, so we can cache it. @@ -66,7 +69,12 @@ def create(self): self.add_constraints() def set_bounds(self, x, y, width, height): - super().set_bounds(x, y, width, height) + if y + height == self.container.height and self.container._safe_bottom: + super().set_bounds( + x, y, width, height + self.container.layout_native.safeAreaInsets.bottom + ) + else: + super().set_bounds(x, y, width, height) # Setting the bounds changes the constraints, but that doesn't mean # the constraints have been fully applied. Schedule a refresh to be done @@ -81,7 +89,9 @@ def content_refreshed(self, container): def add_option(self, index, text, widget, icon=None): # Create the container for the widget - sub_container = ControlledContainer(on_refresh=self.content_refreshed) + sub_container = ControlledContainer( + on_refresh=self.content_refreshed, safe_bottom=True + ) sub_container.content = widget sub_container.enabled = True self.sub_containers.insert(index, sub_container) diff --git a/iOS/src/toga_iOS/widgets/scrollcontainer.py b/iOS/src/toga_iOS/widgets/scrollcontainer.py index ccdaf93f14..f5afba4d3f 100644 --- a/iOS/src/toga_iOS/widgets/scrollcontainer.py +++ b/iOS/src/toga_iOS/widgets/scrollcontainer.py @@ -32,6 +32,10 @@ def refreshContent(self): class ScrollContainer(Widget): + @property + def verticalInsets(self): + return self.native.safeAreaInsets.bottom + def create(self): self.native = TogaScrollView.alloc().init() self.native.interface = self.interface @@ -54,7 +58,12 @@ def set_content(self, widget): self.document_container.content = widget def set_bounds(self, x, y, width, height): - super().set_bounds(x, y, width, height) + if y + height == self.container.height and self.container._safe_bottom: + super().set_bounds( + x, y, width, height + self.container.layout_native.safeAreaInsets.bottom + ) + else: + super().set_bounds(x, y, width, height) # Setting the bounds changes the constraints, but that doesn't mean # the constraints have been fully applied. Schedule a refresh to be done @@ -71,7 +80,10 @@ def content_refreshed(self, container): width = max(self.interface.content.layout.width, width) if self.interface.vertical: - height = max(self.interface.content.layout.height, height) + height = max( + self.interface.content.layout.height, + height + 0.001 * self._allow_vertical, + ) self.native.contentSize = NSMakeSize(width, height)