Skip to content

Commit

Permalink
Fixed UIStackView
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklockwood committed Nov 17, 2017
1 parent 600a8d5 commit c9beb00
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 34 deletions.
4 changes: 4 additions & 0 deletions Layout.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
01EAFD791F60613600B73E92 /* Layout.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 011CE6991F053ABD0034771C /* Layout.framework */; };
01EAFD801F60649500B73E92 /* Symbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EAFD7F1F60649500B73E92 /* Symbols.swift */; };
01F62CCD1F8CF2C60052B8E9 /* OptionSetExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F62CCC1F8CF2C60052B8E9 /* OptionSetExpressionTests.swift */; };
01FAAD271FBD991E00C26799 /* StackViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FAAD261FBD991E00C26799 /* StackViewTests.swift */; };
3583485A1F3F1EB000CFB6BB /* ReturnCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 358348591F3F1EB000CFB6BB /* ReturnCodeTests.swift */; };
3583485B1F3F217200CFB6BB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0105F79C1F055C260067B313 /* main.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -391,6 +392,7 @@
01EAFD781F60613600B73E92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
01EAFD7F1F60649500B73E92 /* Symbols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Symbols.swift; sourceTree = "<group>"; };
01F62CCC1F8CF2C60052B8E9 /* OptionSetExpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionSetExpressionTests.swift; sourceTree = "<group>"; };
01FAAD261FBD991E00C26799 /* StackViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackViewTests.swift; sourceTree = "<group>"; };
05DA996CC29E3F6DB7194413 /* Pods_LayoutTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LayoutTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
358348591F3F1EB000CFB6BB /* ReturnCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnCodeTests.swift; sourceTree = "<group>"; };
4118130BF7EC77593CF35CF1 /* Pods_Layout.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Layout.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -629,6 +631,7 @@
011CE6BF1F053D530034771C /* LayoutFrameTests.swift */,
011CE6C01F053D530034771C /* LayoutNodeTests.swift */,
011CE6C11F053D530034771C /* OptionalExpressionTests.swift */,
01FAAD261FBD991E00C26799 /* StackViewTests.swift */,
011CE6C21F053D530034771C /* StateTests.swift */,
017539881F2A2F2F005B7516 /* XMLTests.swift */,
0192A6CE1FA73BA300A30FDA /* FileTests.swift */,
Expand Down Expand Up @@ -1396,6 +1399,7 @@
0192A6CF1FA73BA300A30FDA /* FileTests.swift in Sources */,
011CE6CB1F053D530034771C /* OptionalExpressionTests.swift in Sources */,
016439C51F8D033B00497FD9 /* ArrayExpressionTests.swift in Sources */,
01FAAD271FBD991E00C26799 /* StackViewTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
98 changes: 64 additions & 34 deletions Layout/LayoutNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ public class LayoutNode: NSObject {
private var _observingFrame = false
private func _stopObservingFrame() {
if _observingFrame {
removeObserver(self, forKeyPath: "_view.translatesAutoresizingMaskIntoConstraints")
removeObserver(self, forKeyPath: "_view.frame")
removeObserver(self, forKeyPath: "_view.bounds")
_observingFrame = false
Expand Down Expand Up @@ -200,6 +201,7 @@ public class LayoutNode: NSObject {
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryChanged), name: .UIContentSizeCategoryDidChange, object: nil)
}
if !_observingFrame {
addObserver(self, forKeyPath: "_view.translatesAutoresizingMaskIntoConstraints", options: [.new, .old], context: nil)
addObserver(self, forKeyPath: "_view.frame", options: .new, context: nil)
addObserver(self, forKeyPath: "_view.bounds", options: .new, context: nil)
_observingFrame = true
Expand Down Expand Up @@ -247,6 +249,10 @@ public class LayoutNode: NSObject {
update()
}
_previousSafeAreaInsets = insets
case let useAutoresizing as Bool:
if useAutoresizing != change?[.oldKey] as? Bool {
update()
}
default:
preconditionFailure()
}
Expand Down Expand Up @@ -832,9 +838,7 @@ public class LayoutNode: NSObject {
}
if !hasExpression("height") {
_getters["height"] = nil
if _class is UIStackView.Type {
expressions["height"] = "auto" // TODO: remove special case
} else if hasExpression("top"), hasExpression("bottom") {
if hasExpression("top"), hasExpression("bottom") {
expressions["height"] = "bottom - top"
} else if !(_view is UIScrollView), _view is UIImageView || _usesAutoLayout ||
_view?.intrinsicContentSize.height != UIViewNoIntrinsicMetric {
Expand All @@ -854,7 +858,6 @@ public class LayoutNode: NSObject {
}

private func cleanUp(recursive: Bool) {
assert(_evaluating.isEmpty)
assert(!_settingUpExpressions)
if let error = _unhandledError, error.isTransient {
_unhandledError = nil
Expand Down Expand Up @@ -1515,6 +1518,9 @@ public class LayoutNode: NSObject {
break
}
}
if self._evaluating.contains("width") {
return 0
}
return try SymbolError.wrap({
let contentInset = try self.computeContentInset()
return try self.cgFloatValue(forSymbol: "width") -
Expand All @@ -1533,6 +1539,9 @@ public class LayoutNode: NSObject {
break
}
}
if self._evaluating.contains("height") {
return 0
}
return try SymbolError.wrap({
let contentInset = try self.computeContentInset()
return try self.cgFloatValue(forSymbol: "height") -
Expand Down Expand Up @@ -1759,10 +1768,10 @@ public class LayoutNode: NSObject {
if !children.isEmpty {
for child in children {
var childSize = CGSize.zero
if !child.widthDependsOnParent {
if !child._evaluating.contains("width") {
childSize.width = try child.cgFloatValue(forSymbol: "width")
}
if !child.heightDependsOnParent {
if !child._evaluating.contains("height") {
childSize.height = try child.cgFloatValue(forSymbol: "height")
}
if isVertical {
Expand All @@ -1778,6 +1787,22 @@ public class LayoutNode: NSObject {
} else {
size.width -= spacing
}
} else {
if _view?.translatesAutoresizingMaskIntoConstraints == false {
if let width = try computeExplicitWidth(), width != 0 {
_widthConstraint?.constant = width
_widthConstraint?.isActive = true
} else {
_widthConstraint?.isActive = false
}
if let height = try computeExplicitHeight(), height != 0 {
_heightConstraint?.constant = height
_heightConstraint?.isActive = true
} else {
_heightConstraint?.isActive = false
}
}
size = _view?.systemLayoutSizeFitting(.zero) ?? .zero
}
return size
}
Expand Down Expand Up @@ -1937,49 +1962,46 @@ public class LayoutNode: NSObject {
return intrinsicSize
}
// Try AutoLayout
if _usesAutoLayout, let _widthConstraint = _widthConstraint, let _heightConstraint = _heightConstraint {
if _usesAutoLayout {
defer { _updateLock -= 1 }
_updateLock += 1
let transform = _view.layer.transform
_view.layer.transform = CATransform3DIdentity
let frame = _view.frame
let usesAutoresizing = _view.translatesAutoresizingMaskIntoConstraints
_view.translatesAutoresizingMaskIntoConstraints = false
_leftConstraint?.isActive = false
_topConstraint?.isActive = false
if let width = try computeExplicitWidth() {
_widthConstraint.isActive = true
_widthConstraint.constant = width
_widthConstraint?.constant = width
_widthConstraint?.isActive = true
} else if intrinsicSize.width != UIViewNoIntrinsicMetric,
_view.constraints.contains(where: { $0.firstAttribute == .width }) {
_widthConstraint.isActive = true
_widthConstraint.constant = intrinsicSize.width
_widthConstraint?.constant = intrinsicSize.width
_widthConstraint?.isActive = true
} else {
_widthConstraint.isActive = false
_widthConstraint?.isActive = false
}
if let height = try computeExplicitHeight() {
_heightConstraint.isActive = true
_heightConstraint.constant = height
_heightConstraint?.constant = height
_heightConstraint?.isActive = true
} else if intrinsicSize.height != UIViewNoIntrinsicMetric,
_view.constraints.contains(where: { $0.firstAttribute == .height }) {
_widthConstraint.isActive = true
_widthConstraint.constant = intrinsicSize.height
_widthConstraint?.constant = intrinsicSize.height
_widthConstraint?.isActive = true
} else {
_heightConstraint.isActive = false
_heightConstraint?.isActive = false
}
_view.layoutIfNeeded()
let size = _view.frame.size
_widthConstraint.isActive = false
_heightConstraint.isActive = false
_view.translatesAutoresizingMaskIntoConstraints = usesAutoresizing
_view.frame = frame
if usesAutoresizing {
_widthConstraint?.isActive = false
_heightConstraint?.isActive = false
_view.translatesAutoresizingMaskIntoConstraints = true
_view.frame = frame
}
_view.layer.transform = transform
if size.width > 0 || size.height > 0 {
return size
}
} else {
_widthConstraint?.isActive = false
_heightConstraint?.isActive = false
}
// Try intrinsic size
var size = intrinsicSize
Expand Down Expand Up @@ -2026,9 +2048,11 @@ public class LayoutNode: NSObject {
// Depends on parent view - must be called again if parent view changes
private func setUpPositionConstraints() {
assert(_topConstraint == nil)
if let parentView = parent?._view {
if let parentView = parent?._view, !(parentView is UIStackView) {
_topConstraint = _view?.topAnchor.constraint(equalTo: parentView.topAnchor, constant: 0)
_topConstraint?.identifier = "LayoutTop"
_leftConstraint = _view?.leftAnchor.constraint(equalTo: parentView.leftAnchor, constant: 0)
_leftConstraint?.identifier = "LayoutLeft"
}
}

Expand Down Expand Up @@ -2060,30 +2084,35 @@ public class LayoutNode: NSObject {
// Note: thrown error is always a LayoutError
private func updateFrame() throws {
guard _updateLock == 0, let _view = _view else { return }
let frame: CGRect
defer {
if parent == nil, _previousBounds != _view.bounds {
_view.superview?.setNeedsLayout()
}
_previousBounds = _view.bounds
_previousBounds = CGRect(
origin: _view.bounds.origin,
size: frame.size
)
_updateLock -= 1
}
_updateLock += 1
let frame = self.frame
frame = self.frame
if frame != _view.frame {
if _view.translatesAutoresizingMaskIntoConstraints {
let transform = _view.layer.transform
_view.layer.transform = CATransform3DIdentity
_view.frame = frame
_view.layer.transform = transform
} else if let _widthConstraint = _widthConstraint, let _heightConstraint = _heightConstraint {
_widthConstraint.constant = frame.width
_widthConstraint.isActive = true
_heightConstraint.constant = frame.height
_heightConstraint.isActive = true
} else {
_widthConstraint?.constant = frame.width
_widthConstraint?.isActive = true
_heightConstraint?.constant = frame.height
_heightConstraint?.isActive = true
_leftConstraint?.constant = frame.origin.x
_leftConstraint?.isActive = true
_topConstraint?.constant = frame.origin.y
_topConstraint?.isActive = true
_view.updateConstraintsIfNeeded()
}
}
if viewClass == UIScrollView.self, // Skip this behavior for subclasses like UITableView
Expand All @@ -2103,6 +2132,7 @@ public class LayoutNode: NSObject {
for child in children {
try LayoutError.wrap(child.updateFrame, for: self)
}
_view.layoutIfNeeded()
_view.didUpdateLayout(for: self)
_view.viewController?.didUpdateLayout(for: self)
try throwUnhandledError()
Expand Down
7 changes: 7 additions & 0 deletions Layout/UIStackView+Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,11 @@ extension UIStackView {
(node._view as UIView?).map(removeArrangedSubview)
super.willRemoveChildNode(node, at: index)
}

open override class var defaultExpressions: [String: String] {
return [
"width": "auto",
"height": "auto",
]
}
}
33 changes: 33 additions & 0 deletions LayoutTests/LayoutFrameTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ class LayoutFrameTests: XCTestCase {
node.update()
XCTAssertTrue(node.frame.width > 100)
XCTAssertTrue(node.frame.height < 30)
XCTAssertEqual(node.frame, node.view.frame)
node = LayoutNode(
view: UILabel(),
expressions: [
Expand All @@ -240,5 +241,37 @@ class LayoutFrameTests: XCTestCase {
node.update()
XCTAssertTrue(node.frame.width <= 50)
XCTAssertTrue(node.frame.height > 20)
XCTAssertEqual(node.frame, node.view.frame)
}

// MARK: AutoLayout

func testAutosizeTextUsingConstraints() {
let text = "This is a line long enough to wrap"
var node = LayoutNode(
view: UILabel(),
expressions: [
"translatesAutoresizingMaskIntoConstraints": "false",
"text": text,
"numberOfLines": "0",
]
)
node.update()
XCTAssertTrue(node.frame.width > 100)
XCTAssertTrue(node.frame.height < 30)
XCTAssertEqual(node.frame.size, node.view.systemLayoutSizeFitting(.zero))
node = LayoutNode(
view: UILabel(),
expressions: [
"translatesAutoresizingMaskIntoConstraints": "false",
"text": text,
"width": "50",
"numberOfLines": "0",
]
)
node.update()
XCTAssertTrue(node.frame.width <= 50)
XCTAssertTrue(node.frame.height > 20)
XCTAssertEqual(node.frame.size, node.view.systemLayoutSizeFitting(.zero))
}
}
Loading

0 comments on commit c9beb00

Please sign in to comment.