Skip to content

Commit

Permalink
Resolve remaining iOS widget memory leaks.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Jan 7, 2025
1 parent 83d8744 commit 83b13eb
Show file tree
Hide file tree
Showing 10 changed files with 28 additions and 14 deletions.
1 change: 1 addition & 0 deletions changes/2849.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Widgets on the iOS backend no longer leak memory when destroyed.
13 changes: 9 additions & 4 deletions iOS/src/toga_iOS/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ def __del__(self): # pragma: nocover
def _remove_constraints(self):
if self.container:
# print(f"Remove constraints for {self.widget} in {self.container}")
self.container.native.removeConstraint(self.width_constraint)
self.container.native.removeConstraint(self.height_constraint)
self.container.native.removeConstraint(self.left_constraint)
self.container.native.removeConstraint(self.top_constraint)
# Due to the unpredictability of garbage collection, it's possible for
# the native object of the window's container to be deleted on the ObjC
# side before the constraints for the window have been removed. Protect
# against this possibility.
if self.container.native:
self.container.native.removeConstraint(self.width_constraint)
self.container.native.removeConstraint(self.height_constraint)
self.container.native.removeConstraint(self.left_constraint)
self.container.native.removeConstraint(self.top_constraint)

@property
def container(self):
Expand Down
5 changes: 5 additions & 0 deletions iOS/src/toga_iOS/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ def __init__(self, content=None, layout_native=None, on_refresh=None):

self.layout_native = self.native if layout_native is None else layout_native

def __del__(self):
# Mark the contained native object as explicitly None so that the
# constraints know the object has been deleted.
self.native = None

@property
def width(self):
return self.layout_native.bounds.size.width
Expand Down
5 changes: 1 addition & 4 deletions iOS/src/toga_iOS/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ class TogaView(UIView):

class Box(Widget):
def create(self):
# This should be a TogaView - but making it a TogaView causes segfaults when
# content is added and removed from containers like OptionContainer and
# ScrollContainer.
self.native = UIView.alloc().init()
self.native = TogaView.alloc().init()
self.native.interface = self.interface
self.native.impl = self

Expand Down
8 changes: 7 additions & 1 deletion iOS/src/toga_iOS/widgets/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@
from toga_iOS.widgets.base import Widget


class TogaBaseTextField(UITextField):
interface = objc_property(object, weak=True)
impl = objc_property(object, weak=True)


class TogaPickerView(UIPickerView):
interface = objc_property(object, weak=True)
impl = objc_property(object, weak=True)
native = objc_property(object, weak=True)

@objc_method
def numberOfComponentsInPickerView_(self, pickerView) -> int:
Expand Down Expand Up @@ -53,7 +59,7 @@ def pickerView_didSelectRow_inComponent_(

class Selection(Widget):
def create(self):
self.native = UITextField.alloc().init()
self.native = TogaBaseTextField.alloc().init()
self.native.interface = self.interface
self.native.impl = self
self.native.tintColor = UIColor.clearColor
Expand Down
2 changes: 1 addition & 1 deletion testbed/tests/widgets/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ async def widget():
return toga.Box(style=Pack(width=100, height=200))


test_cleanup = build_cleanup_test(toga.Box, xfail_platforms=("iOS",))
test_cleanup = build_cleanup_test(toga.Box)
2 changes: 1 addition & 1 deletion testbed/tests/widgets/test_imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async def widget():


test_cleanup = build_cleanup_test(
toga.ImageView, kwargs={"image": "resources/sample.png"}, xfail_platforms=("iOS",)
toga.ImageView, kwargs={"image": "resources/sample.png"}
)


Expand Down
2 changes: 1 addition & 1 deletion testbed/tests/widgets/test_optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async def widget(content1, content2, content3, on_select_handler):
# Pass a function here to prevent init of toga.Box() in a different thread than
# toga.OptionContainer. This would raise a runtime error on Windows.
lambda: toga.OptionContainer(content=[("Tab 1", toga.Box())]),
xfail_platforms=("android", "iOS", "linux"),
xfail_platforms=("android", "linux"),
)


Expand Down
2 changes: 1 addition & 1 deletion testbed/tests/widgets/test_scrollcontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ async def widget(content, on_scroll):
# Pass a function here to prevent init of toga.Box() in a different thread than
# toga.ScrollContainer. This would raise a runtime error on Windows.
lambda: toga.ScrollContainer(content=toga.Box()),
xfail_platforms=("android", "iOS", "linux"),
xfail_platforms=("android", "linux"),
)


Expand Down
2 changes: 1 addition & 1 deletion testbed/tests/widgets/test_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def verify_vertical_text_align():
test_cleanup = build_cleanup_test(
toga.Selection,
kwargs={"items": ["first", "second", "third"]},
xfail_platforms=("android", "iOS", "windows"),
xfail_platforms=("android", "windows"),
)


Expand Down

0 comments on commit 83b13eb

Please sign in to comment.