diff --git a/Sources/StackKit/HStackView.swift b/Sources/StackKit/HStackView.swift index 5f02e32..a1f6157 100644 --- a/Sources/StackKit/HStackView.swift +++ b/Sources/StackKit/HStackView.swift @@ -78,9 +78,17 @@ open class HStackView: UIView { self.isHidden = effectiveSubviews.isEmpty } + private func tryResizeStackView() { + subviews.forEach { fitSize in + fitSize._fitSize(with: fitSize.stackKitFitType) + } + } + open override func layoutSubviews() { super.layoutSubviews() + tryResizeStackView() + switch alignment { case .top: effectiveSubviews.forEach { $0.frame.origin.y = 0 } @@ -123,7 +131,8 @@ open class HStackView: UIView { } open override func sizeThatFits(_ size: CGSize) -> CGSize { - layoutSubviews() + setNeedsLayout() + layoutIfNeeded() var _size = size if size.width == CGFloat.greatestFiniteMagnitude || size.width == 0 { @@ -174,7 +183,11 @@ extension HStackView { private func autoSpacing() -> CGFloat { let unspacerViews = viewsWithoutSpacer() let spacersCount = spacerViews().map({ isSpacerBetweenViews($0) }).filter({ $0 }).count - return (frame.width - viewsWidth() - spacerSpecifyLength()) / CGFloat(unspacerViews.count - spacersCount - 1) + let number = unspacerViews.count - spacersCount - 1 + if number <= 0 { + return 0 + } + return (frame.width - viewsWidth() - spacerSpecifyLength()) / CGFloat(number) } private func viewsWidth() -> CGFloat { @@ -254,19 +267,13 @@ extension HStackView { return false } - var isPreviousView = false - var isNextView = false - - let previous = index - 1 - if previous > 0, previous < effectiveSubviews.count - 1 { - isPreviousView = true + guard effectiveSubviews.count >= 3 else { + return false } - let next = index + 1 - if next < effectiveSubviews.count - 1 { - isNextView = true - } - return isPreviousView && isNextView + let start: Int = 1 + let end: Int = effectiveSubviews.count - 2 + return (start ... end).contains(index) } private func fillSpecifySpacer() { diff --git a/Sources/StackKit/Runtime.swift b/Sources/StackKit/Runtime.swift new file mode 100644 index 0000000..017b0e1 --- /dev/null +++ b/Sources/StackKit/Runtime.swift @@ -0,0 +1,38 @@ +// +// File.swift +// +// +// Created by i on 2022/8/11. +// + +import UIKit + +struct Runtime { + init() { } +} + +extension Runtime { + + static func getProperty(_ object: Any, key: UnsafeRawPointer) -> Any? { + objc_getAssociatedObject(object, key) + } + + static func setProperty(_ object: Any, key: UnsafeRawPointer, value: Any?, policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) { + objc_setAssociatedObject(object, key, value, policy) + } + + static func getCGFloatProperty(_ object: Any, key: UnsafeRawPointer) -> CGFloat? { + guard let value = getProperty(object, key: key) as? NSNumber else { + return nil + } + return CGFloat(truncating: value) + } + + static func setCGFloatProperty(_ object: Any, key: UnsafeRawPointer, _ value: CGFloat?) { + guard let v = value else { + objc_setAssociatedObject(object, key, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return + } + objc_setAssociatedObject(object, key, NSNumber(value: Double(v)), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } +} diff --git a/Sources/StackKit/Spacer.swift b/Sources/StackKit/Spacer.swift index 7d80013..b79e5f6 100644 --- a/Sources/StackKit/Spacer.swift +++ b/Sources/StackKit/Spacer.swift @@ -32,7 +32,7 @@ class SpacerView: UIView, _Spacer { var min: CGFloat = .leastNonzeroMagnitude var max: CGFloat = .greatestFiniteMagnitude - var setLength: CGFloat = -1 + var setLength: CGFloat = 0 required init(length: CGFloat, min: CGFloat, max: CGFloat) { super.init(frame: .zero) @@ -73,15 +73,15 @@ class SpacerView: UIView, _Spacer { switch size { case .width: - frame.size.width = length + frame.size.width = Swift.max(0, length) case .height: - frame.size.height = length + frame.size.height = Swift.max(0, length) } - self.setLength = length + self.setLength = Swift.max(0, length) } var minLength: CGFloat { - if setLength != -1 { + if setLength != 0 { return setLength } if length != .greatestFiniteMagnitude { @@ -107,7 +107,7 @@ class SpacerLayer: CALayer, _Spacer { var min: CGFloat = .leastNonzeroMagnitude var max: CGFloat = .greatestFiniteMagnitude - var setLength: CGFloat = -1 + var setLength: CGFloat = 0 required init(length: CGFloat, min: CGFloat, max: CGFloat) { super.init() @@ -150,15 +150,15 @@ class SpacerLayer: CALayer, _Spacer { switch size { case .width: - frame.size.width = length + frame.size.width = Swift.max(0, length) case .height: - frame.size.height = length + frame.size.height = Swift.max(0, length) } - self.setLength = length + self.setLength = Swift.max(0, length) } var minLength: CGFloat { - if setLength != -1 { + if setLength != 0 { return setLength } if length != .greatestFiniteMagnitude { diff --git a/Sources/StackKit/StackKitResultBuilders.swift b/Sources/StackKit/StackKitResultBuilders.swift index f64aa76..7630c74 100644 --- a/Sources/StackKit/StackKitResultBuilders.swift +++ b/Sources/StackKit/StackKitResultBuilders.swift @@ -21,6 +21,9 @@ extension _StackKitViewContentResultBuilderProvider { public static func buildExpression(_ expression: Void) -> [UIView] { [] } + public static func buildExpression(_ expression: StackKitCompatible) -> [T] where T: UIView { + [expression.view] + } } // MARK: For VStack View diff --git a/Sources/StackKit/UIKit+FitSize.swift b/Sources/StackKit/UIKit+FitSize.swift new file mode 100644 index 0000000..ce30250 --- /dev/null +++ b/Sources/StackKit/UIKit+FitSize.swift @@ -0,0 +1,182 @@ +import UIKit + +var _FitTypeKey = "_FitTypeKey" + +public enum FitType { + case content + + case width + case height + + case widthFlexible + case heightFlexible + + var isFlexible: Bool { + if case .widthFlexible = self { + return true + } else if case .heightFlexible = self { + return true + } + return false + } +} + +protocol FitSize { + var stackKitFitType: FitType? { get set } + func _fitSize(with fitType: FitType?) +} + +extension UIView: FitSize { + + var stackKitFitType: FitType? { + get { + objc_getAssociatedObject(self, &_FitTypeKey) as? FitType + } + set { + objc_setAssociatedObject(self, &_FitTypeKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) + } + } + + func fitSize(with fitType: FitType) -> Self { + self.stackKitFitType = fitType + return self + } + + func _fitSize(with fitType: FitType? = .content) { + + defer { + setNeedsLayout() + } + + var size = resolveSize() + + if let w = size.width, let h = size.height { // 指定了 size(width & height) 优先使用 size + self.frame.size = CGSize(width: w, height: h) + return + } + + guard let fitType = fitType else { + self.frame.size = sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: .greatestFiniteMagnitude)) + return + } + + var fitWidth = CGFloat.greatestFiniteMagnitude + var fitHeight = CGFloat.greatestFiniteMagnitude + + switch fitType { + case .width, .widthFlexible: + if let w = applyMinMax(toWidth: size.width) { + fitWidth = w + } else { + fitWidth = bounds.width + } + case .height, .heightFlexible: + if let h = applyMinMax(toHeight: size.height) { + fitHeight = h + } else { + fitHeight = bounds.height + } + case .content: + fitWidth = size.width ?? bounds.width + fitHeight = size.height ?? bounds.height + } + + fitWidth = _validateValue(fitWidth) + fitHeight = _validateValue(fitHeight) + + let sizeThatFits = sizeThatFits(CGSize(width: fitWidth, height: fitHeight)) + + switch fitType { + case .width, .height, .widthFlexible, .heightFlexible: + if fitWidth != .greatestFiniteMagnitude { + size.width = fitType.isFlexible ? sizeThatFits.width : fitWidth + } else { + size.width = sizeThatFits.width + } + + if fitHeight != .greatestFiniteMagnitude { + size.height = fitType.isFlexible ? sizeThatFits.height : fitHeight + } else { + size.height = sizeThatFits.height + } + case .content: + size = Size(width: sizeThatFits.width, height: sizeThatFits.height) + } + + size.width = applyMinMax(toWidth: size.width) + size.height = applyMinMax(toHeight: size.height) + + self.frame.size = size.cgSize + } + + private func _validateValue(_ value: CGFloat?) -> CGFloat { + guard let value = value, value > 0, value.isFinite else { + return .greatestFiniteMagnitude + } + return value + } + + private func _fixedSize(_ size: inout Size) { + if let w = _width { + size.width = w + } + if let h = _height { + size.height = h + } + } +} + +extension UIView { + + struct Size { + var width: CGFloat? + var height: CGFloat? + + var cgSize: CGSize { + CGSize(width: width ?? 0, height: height ?? 0) + } + } + + func resolveSize() -> Size { + var size = Size() + if let _width = _width { + size.width = _width + } + if let _height = _height { + size.height = _height + } + return size + } + + func applyMinMax(toWidth width: CGFloat?) -> CGFloat? { + var result = width + + // Handle minWidth + if let minWidth = _minWidth, minWidth > (result ?? 0) { + result = minWidth + } + + // Handle maxWidth + if let maxWidth = _maxWidth, maxWidth < (result ?? CGFloat.greatestFiniteMagnitude) { + result = maxWidth + } + return result + } + + func applyMinMax(toHeight height: CGFloat?) -> CGFloat? { + var result = height + + // Handle minWidth + if let minWidth = _minHeight, minWidth > (result ?? 0) { + result = minWidth + } + + // Handle maxWidth + if let maxWidth = _maxHeight, maxWidth < (result ?? CGFloat.greatestFiniteMagnitude) { + result = maxWidth + } + + return result + } + +} diff --git a/Sources/StackKit/UIKit+StackKit.swift b/Sources/StackKit/UIKit+StackKit.swift new file mode 100644 index 0000000..9c6de2b --- /dev/null +++ b/Sources/StackKit/UIKit+StackKit.swift @@ -0,0 +1,170 @@ +import UIKit + +struct _UIView_StackKitKeys { + static var widthKey = "StackKit_widthKey" + static var heightKey = "StackKit_heightKey" + + static var minWidthKey = "StackKit_minWidthKey" + static var maxWidthKey = "StackKit_maxWidthKey" + + static var minHeightKey = "StackKit_minHeightKey" + static var maxHeightKey = "StackKit_maxHeightKey" +} + +protocol _UIView_StackKitProvider { + var _width: CGFloat? { get set } + var _height: CGFloat? { get set } + + var _minWidth: CGFloat? { get set } + var _maxWidth: CGFloat? { get set } + + var _minHeight: CGFloat? { get set } + var _maxHeight: CGFloat? { get set } +} + +extension UIView: _UIView_StackKitProvider { + var _width: CGFloat? { + get { + guard let value = Runtime.getCGFloatProperty(self, key: &_UIView_StackKitKeys.widthKey) else { + return nil + } + return value + } + set { + Runtime.setCGFloatProperty(self, key: &_UIView_StackKitKeys.widthKey, newValue) + } + } + var _height: CGFloat? { + get { + guard let value = Runtime.getCGFloatProperty(self, key: &_UIView_StackKitKeys.heightKey) else { + return nil + } + return value + } + set { + Runtime.setCGFloatProperty(self, key: &_UIView_StackKitKeys.heightKey, newValue) + } + } + var _minWidth: CGFloat? { + get { + guard let value = Runtime.getCGFloatProperty(self, key: &_UIView_StackKitKeys.minWidthKey) else { + return nil + } + return value + } + set { + Runtime.setCGFloatProperty(self, key: &_UIView_StackKitKeys.minWidthKey, newValue) + } + } + + var _maxWidth: CGFloat? { + get { + guard let value = Runtime.getCGFloatProperty(self, key: &_UIView_StackKitKeys.maxWidthKey) else { + return nil + } + return value + } + set { + Runtime.setCGFloatProperty(self, key: &_UIView_StackKitKeys.maxWidthKey, newValue) + } + } + + var _minHeight: CGFloat? { + get { + guard let value = Runtime.getCGFloatProperty(self, key: &_UIView_StackKitKeys.minHeightKey) else { + return nil + } + return value + } + set { + Runtime.setCGFloatProperty(self, key: &_UIView_StackKitKeys.minHeightKey, newValue) + } + } + + var _maxHeight: CGFloat? { + get { + guard let value = Runtime.getCGFloatProperty(self, key: &_UIView_StackKitKeys.maxHeightKey) else { + return nil + } + return value + } + set { + Runtime.setCGFloatProperty(self, key: &_UIView_StackKitKeys.maxHeightKey, newValue) + } + } +} + +public class StackKitCompatible { + let view: Base + init(view: Base) { + self.view = view + } +} + +public protocol StackKitCompatibleProvider { + associatedtype O + var stack: O { get } + static var stack: O { get } +} + +public extension StackKitCompatibleProvider where Self: UIView { + var stack: StackKitCompatible { + return StackKitCompatible(view: self) + } + static var stack: StackKitCompatible { + return StackKitCompatible(view: Self.init()) + } +} + +extension UIView: StackKitCompatibleProvider { } + + +extension StackKitCompatible where Base: UIView { + + public func width(_ value: CGFloat?) -> Self { + view._width = value + return self + } + public func height(_ value: CGFloat?) -> Self { + view._height = value + return self + } + + public func maxWidth(_ value: CGFloat?) -> Self { + view._maxWidth = value + return self + } + public func maxHeight(_ value: CGFloat?) -> Self { + view._maxHeight = value + return self + } + public func minWidth(_ value: CGFloat?) -> Self { + view._minWidth = value + return self + } + public func minHeight(_ value: CGFloat?) -> Self { + view._minHeight = value + return self + } + + public func size(_ length: CGFloat) -> Self { + view._width = length + view._height = length + return self + } + + public func size(_ width: CGFloat, _ height: CGFloat) -> Self { + view._width = width + view._height = height + return self + } + + public func sizeToFit(_ fitType: FitType = .content) -> Self { + view.stackKitFitType = fitType + return self + } + public func then(_ then: (Base) -> Void) -> Self { + then(self.view) + return self + } +} diff --git a/Sources/StackKit/VStackView.swift b/Sources/StackKit/VStackView.swift index 429671a..80de3aa 100644 --- a/Sources/StackKit/VStackView.swift +++ b/Sources/StackKit/VStackView.swift @@ -78,9 +78,17 @@ open class VStackView: UIView { self.isHidden = effectiveSubviews.isEmpty } + private func tryResizeStackView() { + subviews.forEach { fitSize in + fitSize._fitSize(with: fitSize.stackKitFitType) + } + } + open override func layoutSubviews() { super.layoutSubviews() + tryResizeStackView() + switch alignment { case .left: effectiveSubviews.forEach { $0.frame.origin.x = 0 } @@ -123,7 +131,8 @@ open class VStackView: UIView { } open override func sizeThatFits(_ size: CGSize) -> CGSize { - layoutSubviews() + setNeedsLayout() + layoutIfNeeded() var _size = size if size.width == CGFloat.greatestFiniteMagnitude || size.width == 0 { @@ -174,7 +183,11 @@ extension VStackView { private func autoSpacing() -> CGFloat { let unspacerViews = viewsWithoutSpacer() let spacersCount = spacerViews().map({ isSpacerBetweenViews($0) }).filter({ $0 }).count - return (frame.height - viewsHeight() - spacerSpecifyLength()) / CGFloat(unspacerViews.count - spacersCount - 1) + let number = unspacerViews.count - spacersCount - 1 + if number <= 0 { + return 0 + } + return (frame.height - viewsHeight() - spacerSpecifyLength()) / CGFloat( number) } private func viewsHeight() -> CGFloat { @@ -252,23 +265,17 @@ extension VStackView { } private func isSpacerBetweenViews(_ spacer: SpacerView) -> Bool { - guard let index = subviews.firstIndex(of: spacer) else { + guard let index = effectiveSubviews.firstIndex(of: spacer) else { return false } - var isPreviousView = false - var isNextView = false - - let previous = index - 1 - if previous > 0, previous < subviews.count - 1 { - isPreviousView = true + guard effectiveSubviews.count >= 3 else { + return false } - let next = index + 1 - if next < subviews.count - 1 { - isNextView = true - } - return isPreviousView && isNextView + let start: Int = 1 + let end: Int = effectiveSubviews.count - 2 + return (start ... end).contains(index) } /// 填充 spacer 最小值 diff --git a/Tests/StackKitTests/StackKitTests.swift b/Tests/StackKitTests/StackKitTests.swift index 8726459..ba661ae 100644 --- a/Tests/StackKitTests/StackKitTests.swift +++ b/Tests/StackKitTests/StackKitTests.swift @@ -6,6 +6,5 @@ final class StackKitTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - } }