Skip to content

Instantly share code, notes, and snippets.

@zrzka
Last active Jan 4, 2018
Embed
What would you like to do?
Pythonista & auto layout
#!python3
import ui
from objc_util import ObjCInstance, ObjCClass, on_main_thread
from enum import Enum
from functools import partial
from collections import defaultdict
_LayoutConstraint = ObjCClass('NSLayoutConstraint')
class LayoutRelation(int, Enum):
lessThanOrEqual = -1
equal = 0
greaterThanOrEqual = 1
class LayoutAttribute(int, Enum):
notAnAttribute = 0
left = 1
right = 2
top = 3
bottom = 4
leading = 5
trailing = 6
width = 7
height = 8
centerX = 9
centerY = 10
baseline = 11
lastBaseline = 12
firstBaseline = 13
leftMargin = 14
rightMargin = 15
topMargin = 16
bottomMargin = 17
leadingMargin = 18
trailingMargin = 19
centerXWithinMargins = 20
centerYWithinMargins = 21
class LayoutConstraintOrientation(int, Enum):
horizontal = 0
vertical = 1
class LayoutPriority(float, Enum):
required = 1000
defaultHight = 750
defaultLow = 250
fittingSizeLevel = 50
class _LayoutBaseAttribute:
_relations = {
"min": LayoutRelation.greaterThanOrEqual,
"max": LayoutRelation.lessThanOrEqual,
"equal": LayoutRelation.equal
}
def __init__(self, view, attribute, other=None, other_attribute=LayoutAttribute.notAnAttribute):
assert(isinstance(view, LayoutView))
assert(isinstance(attribute, LayoutAttribute))
self._view = view
self._attribute = attribute
self._constraints = {}
if other:
assert(isinstance(other, ui.View))
assert(isinstance(other_attribute, LayoutAttribute))
self._other = other
self._other_attribute = other_attribute
else:
self._other = None
self._other_attribute = LayoutAttribute.notAnAttribute
@property
def view(self):
return self._view
@property
def attribute(self):
return self._attribute
@property
def other(self):
return self._other
@property
def other_attribute(self):
return self._other_attribute
@property
def superview(self):
return self._view.superview
@on_main_thread
def remove_constraint(self, constraint):
ObjCInstance(self.superview).removeConstraint_(constraint)
@on_main_thread
def add_constraint(self, constraint):
ObjCInstance(self.superview).addConstraint_(constraint)
def constraint(relation, value, priority):
raise NotImplementedError
def __setattr__(self, name, value):
if name in self._relations.keys():
constraint = self._constraints.get(name, None)
if constraint:
self.remove_constraint(constraint)
if value is None:
return
if isinstance(value, tuple):
priority = value[1]
value = value[0]
else:
priority = LayoutPriority.required
constraint = self.constraint(self._relations[name], value, priority)
self._constraints[name] = constraint
self.add_constraint(constraint)
else:
super().__setattr__(name, value)
class _LayoutConstantAttribute(_LayoutBaseAttribute):
def __init__(self, view, attribute, other=None, other_attribute=LayoutAttribute.notAnAttribute):
super().__init__(view, attribute, other, other_attribute)
def constraint(self, relation, value, priority):
constraint = _LayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self.view, int(self.attribute), int(relation), self.other, int(self.other_attribute), 1.0, value
)
constraint.setPriority_(priority)
return constraint
class _LayoutMultiplierAttribute(_LayoutBaseAttribute):
def __init__(self, view, attribute, other=None, other_attribute=LayoutAttribute.notAnAttribute):
super().__init__(view, attribute, other, other_attribute)
def constraint(self, relation, value, priority):
constraint = _LayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self.view, int(self.attribute), int(relation), self.other, int(self.other_attribute), value, 0
)
constraint.setPriority_(priority)
return constraint
class Layout:
_definitions = {
"width": (_LayoutConstantAttribute, LayoutAttribute.width, LayoutAttribute.notAnAttribute),
"height": (_LayoutConstantAttribute, LayoutAttribute.height, LayoutAttribute.notAnAttribute),
"align_center_x_to": (_LayoutConstantAttribute, LayoutAttribute.centerX, LayoutAttribute.centerX),
"align_center_y_to": (_LayoutConstantAttribute, LayoutAttribute.centerY, LayoutAttribute.centerY),
"align_leading_to": (_LayoutConstantAttribute, LayoutAttribute.leading, LayoutAttribute.leading),
"align_trailing_to": (_LayoutConstantAttribute, LayoutAttribute.trailing, LayoutAttribute.trailing),
"align_top_to": (_LayoutConstantAttribute, LayoutAttribute.top, LayoutAttribute.top),
"align_bottom_to": (_LayoutConstantAttribute, LayoutAttribute.bottom, LayoutAttribute.bottom),
"align_left_to": (_LayoutConstantAttribute, LayoutAttribute.left, LayoutAttribute.left),
"align_right_to": (_LayoutConstantAttribute, LayoutAttribute.right, LayoutAttribute.right),
"align_baseline_to": (_LayoutConstantAttribute, LayoutAttribute.baseline, LayoutAttribute.baseline),
"align_center_x_with_superview": (_LayoutConstantAttribute, LayoutAttribute.centerX, LayoutAttribute.centerX),
"align_center_y_with_superview": (_LayoutConstantAttribute, LayoutAttribute.centerY, LayoutAttribute.centerY),
"align_leading_with_superview": (_LayoutConstantAttribute, LayoutAttribute.leading, LayoutAttribute.leading),
"align_trailing_with_superview": (_LayoutConstantAttribute, LayoutAttribute.trailing, LayoutAttribute.trailing),
"align_top_with_superview": (_LayoutConstantAttribute, LayoutAttribute.top, LayoutAttribute.top),
"align_bottom_with_superview": (_LayoutConstantAttribute, LayoutAttribute.bottom, LayoutAttribute.bottom),
"align_left_with_superview": (_LayoutConstantAttribute, LayoutAttribute.left, LayoutAttribute.left),
"align_right_with_superview": (_LayoutConstantAttribute, LayoutAttribute.right, LayoutAttribute.right),
"relative_superview_width": (_LayoutMultiplierAttribute, LayoutAttribute.width, LayoutAttribute.width),
"relative_superview_height": (_LayoutMultiplierAttribute, LayoutAttribute.height, LayoutAttribute.height),
"relative_width_to": (_LayoutMultiplierAttribute, LayoutAttribute.width, LayoutAttribute.width),
"relative_height_to": (_LayoutMultiplierAttribute, LayoutAttribute.height, LayoutAttribute.height),
"left_offset_to": (_LayoutConstantAttribute, LayoutAttribute.left, LayoutAttribute.right),
"right_offset_to": (_LayoutConstantAttribute, LayoutAttribute.right, LayoutAttribute.left),
"top_offset_to": (_LayoutConstantAttribute, LayoutAttribute.top, LayoutAttribute.bottom),
"bottom_offset_to": (_LayoutConstantAttribute, LayoutAttribute.bottom, LayoutAttribute.top),
"leading_offset_to": (_LayoutConstantAttribute, LayoutAttribute.leading, LayoutAttribute.trailing),
"trailing_offset_to": (_LayoutConstantAttribute, LayoutAttribute.trailing, LayoutAttribute.leading)
}
def __init__(self, view):
assert(isinstance(view, LayoutView))
self._view = view
self._attributes = {}
def _create_attribute(self, cls, attribute, other_attribute, other):
assert(isinstance(attribute, LayoutAttribute))
if other:
assert(isinstance(other_attribute, LayoutAttribute))
assert(isinstance(other, ui.View))
name = '{}{}{}'.format(int(attribute), id(other), int(other_attribute))
layout_attribute = self._attributes.get(name, None)
if layout_attribute:
return layout_attribute
layout_attribute = cls(self._view, attribute, other, other_attribute)
self._attributes[name] = layout_attribute
return layout_attribute
def _attribute(self, name, definition):
cls = definition[0]
attribute = definition[1]
other_attribute = definition[2]
assert(isinstance(attribute, LayoutAttribute))
assert(isinstance(other_attribute, LayoutAttribute))
if 'superview' in name:
return self._create_attribute(cls, attribute, other_attribute, self._view.superview)
elif name.endswith('_to'):
return partial(self._create_attribute, cls, attribute, other_attribute)
else:
assert(other_attribute is LayoutAttribute.notAnAttribute)
return self._create_attribute(cls, attribute, other_attribute, None)
def __getattr__(self, name):
if name in self._definitions:
return self._attribute(name, self._definitions[name])
return super().__getattr__(name)
class LayoutView(ui.View):
def __init__(self, view):
assert(isinstance(view, ui.View))
self._view = view
self.add_subview(self._view)
self._view_objc = ObjCInstance(self._view)
self._objc = ObjCInstance(self)
self._view_objc.setTranslatesAutoresizingMaskIntoConstraints_(False)
self._objc.setTranslatesAutoresizingMaskIntoConstraints_(False)
attributes = [LayoutAttribute.left, LayoutAttribute.right, LayoutAttribute.top, LayoutAttribute.bottom]
for attribute in attributes:
constraint = _LayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self._view_objc, int(attribute), int(LayoutRelation.equal), self._objc, int(attribute), 1.0, 0
)
self._objc.addConstraint_(constraint)
self._layout = None
@property
def layout(self):
if not self._layout:
self._layout = Layout(self)
return self._layout
@property
def view(self):
return self._view
def table_like():
view = ui.View(background_color='white')
name_label = LayoutView(ui.Label(
text='Name', font=('<system-bold>', 18), background_color=(0, 0, 0, 0.1)
))
view.add_subview(name_label)
# 15 points from superview left edge
name_label.layout.align_left_with_superview.equal = 15
# 15 points from superview top edge
name_label.layout.align_top_with_superview.equal = 15
age_label = LayoutView(ui.Label(
text='Age', font=('<system-bold>', 18),
alignment=ui.ALIGN_RIGHT, background_color=(0, 0, 0, 0.1)
))
view.add_subview(age_label)
# Align label baseline with name label
age_label.layout.align_baseline_to(name_label).equal = 0
# Right side of age label is 15 points from superview right edge
age_label.layout.align_right_with_superview.equal = -15
# Age label width is always 150 pixel
age_label.layout.width.equal = (150, LayoutPriority.required)
age_label.layout.leading_offset_to(name_label).equal = 8
last_name_label = name_label
last_age_label = age_label
for i in range(1, 5):
name_label = LayoutView(ui.Label(
text='Dummy Name {}'.format(i), background_color=(0, 0, 0, 0.01)
))
view.add_subview(name_label)
# Left egde aligned with left egde of last label
name_label.layout.align_leading_to(last_name_label).equal = 0
# Top edge aligned with bottom edge of last label + 16 points
name_label.layout.top_offset_to(last_name_label).equal = 16
# Width is 100% of last name label width
name_label.layout.relative_width_to(last_name_label).equal = 1.0
age_label = LayoutView(ui.Label(
text=str(i * 5), alignment=ui.ALIGN_RIGHT, background_color=(0, 0, 0, 0.01)
))
view.add_subview(age_label)
# Baseline aligned with name label
age_label.layout.align_baseline_to(name_label).equal = 0
# Right edge aligned with last age label
age_label.layout.align_trailing_to(last_age_label).equal = 0
# Width is 100% of width of last age label
age_label.layout.relative_width_to(last_age_label).equal = 1.0
last_name_label = name_label
last_age_label = age_label
return view
def center_in_superview():
view = ui.View(background_color='white')
label = LayoutView(ui.Label(
text='80% of width, height, centered', alignment=ui.ALIGN_CENTER,
background_color=(0, 0, 0, 0.1)
))
view.add_subview(label)
# Align horizontal, vertical center with superview
label.layout.align_center_x_with_superview.equal = 0
label.layout.align_center_y_with_superview.equal = 0
# Width and height 80% of superview width and height
label.layout.relative_width_to(view).equal = 0.8
label.layout.relative_height_to(view).equal = 0.8
return view
def grid_like():
view = ui.View(background_color='white')
rows = 3
cols = 3
spacing = 10
cells = defaultdict(list)
for row in range(0, rows):
for col in range(0, cols):
cell = LayoutView(ui.Label(
text='R: {} C: {}'.format(row, col),
alignment=ui.ALIGN_CENTER,
background_color=(0, 0, 0, 0.1)
))
view.add_subview(cell)
cells[row].append(cell)
if row == 0:
# Top cells, align to superview top
cell.layout.align_top_to(view).equal = spacing
if row == rows - 1:
# Bottom cells, align to superview bottom
cell.layout.align_bottom_to(view).equal = -spacing
if col == 0:
# Left column cells, align to superview left
cell.layout.align_left_to(view).equal = spacing
if col == cols - 1:
# Right column cells, align to superview right
cell.layout.align_right_to(view).equal = -spacing
if row > 0 or col > 0:
# Not top/left cell, make same width / height as top/left cell
cell.layout.relative_width_to(cells[0][0]).equal = 1.0
cell.layout.relative_height_to(cells[0][0]).equal = 1.0
if col > 0:
# Not first column, add spacing between cells
cell.layout.left_offset_to(cells[row][col - 1]).equal = spacing
if row > 0:
# Not first row, add spacing between cells
cell.layout.top_offset_to(cells[row - 1][col]).equal = spacing
return view
def breadcrumb():
view = ui.View(background_color='white')
spacing = 8
previous_label = None
for title in ('Documents', 'blackmamba', 'experimental', '__init__.py'):
label = LayoutView(ui.Label(
text=title,
background_color=(0, 0, 0, 0.1)
))
view.add_subview(label)
if previous_label:
label.layout.align_baseline_to(previous_label).equal = 0
label.layout.leading_offset_to(previous_label).equal = spacing
else:
label.layout.align_top_to(view).equal = spacing
label.layout.align_left_to(view).equal = spacing
previous_label = label
return view
def main():
datasource = ui.ListDataSource([
{
'title': 'Table',
'view': table_like
},
{
'title': 'Center in superview',
'view': center_in_superview
},
{
'title': 'Grid',
'view': grid_like
},
{
'title': 'Breadcrumb',
'view': breadcrumb
}
])
def did_select(ds):
nv = ds.tableview.navigation_view
tv_objc = ObjCInstance(ds.tableview)
index_path = tv_objc.indexPathForSelectedRow()
if index_path:
tv_objc.deselectRowAtIndexPath_animated_(index_path, True)
row = ds.selected_row
if row < 0:
return
item = ds.items[row]
view = item['view']()
view.name = item['title']
nv.push_view(view)
datasource.action = did_select
window_size = ui.get_window_size()
view = ui.View(width=window_size[0] * 0.8, height=window_size[1] * 0.8)
tv = ui.TableView(frame=view.bounds, flex='WH')
tv.name = 'Auto Layout Demo'
tv.data_source = datasource
tv.delegate = datasource
nv = ui.NavigationView(tv, frame=view.bounds, flex='WH')
view.add_subview(nv)
view.present('sheet')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment