Skip to content

Add FlexFrameLayout support #315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ if linkCoreUI {

let symbolLocatorCondition = envEnable("OPENGSWIFTUI_SYMBOL_LOCATOR", default: buildForDarwinPlatform)

// MARK: - [env] OPENSWIFTUI_OPENCOMBINE

let openCombineCondition = envEnable("OPENSWIFTUI_OPENCOMBINE", default: !buildForDarwinPlatform)

// MARK: - [env] OPENSWIFTUI_SWIFT_LOG

let swiftLogCondition = envEnable("OPENSWIFTUI_SWIFT_LOG", default: !buildForDarwinPlatform)

// MARK: - [env] OPENSWIFTUI_SWIFT_CRYPTO

let swiftCryptoCondition = envEnable("OPENSWIFTUI_SWIFT_CRYPTO", default: !buildForDarwinPlatform)

// MARK: - [env] OPENGSWIFTUI_SWIFTUI_RENDER

let swiftUIRenderCondition = envEnable("OPENSWIFTUI_SWIFTUI_RENDER", default: buildForDarwinPlatform)
Expand Down Expand Up @@ -127,7 +139,7 @@ if warningsAsErrorsCondition {
// MARK: - [env] OPENSWIFTUI_LIBRARY_EVOLUTION

let libraryEvolutionCondition = envEnable("OPENSWIFTUI_LIBRARY_EVOLUTION", default: buildForDarwinPlatform)
if libraryEvolutionCondition {
if libraryEvolutionCondition && !openCombineCondition && !swiftLogCondition {
// NOTE: -enable-library-evolution will cause module verify failure for `swift build`.
// Either set OPENSWIFTUI_LIBRARY_EVOLUTION=0 or add `-Xswiftc -no-verify-emitted-module-interface` after `swift build`
sharedSwiftSettings.append(.unsafeFlags(["-enable-library-evolution", "-no-verify-emitted-module-interface"]))
Expand Down Expand Up @@ -326,6 +338,7 @@ let openSwiftUISymbolDualTestsTarget = Target.testTarget(
"OpenSwiftUI",
"OpenSwiftUITestsSupport",
"OpenSwiftUISymbolDualTestsSupport",
.product(name: "Numerics", package: "swift-numerics"),
],
exclude: ["README.md"],
cSettings: sharedCSettings,
Expand Down Expand Up @@ -502,7 +515,6 @@ if useLocalDeps {
package.dependencies += dependencies
}

let openCombineCondition = envEnable("OPENSWIFTUI_OPENCOMBINE", default: !buildForDarwinPlatform)
if openCombineCondition {
package.dependencies.append(
.package(url: "https://github.com/OpenSwiftUIProject/OpenCombine.git", from: "0.15.0")
Expand All @@ -511,7 +523,6 @@ if openCombineCondition {
openSwiftUITarget.addOpenCombineSettings()
}

let swiftLogCondition = envEnable("OPENSWIFTUI_SWIFT_LOG", default: !buildForDarwinPlatform)
if swiftLogCondition {
package.dependencies.append(
.package(url: "https://github.com/apple/swift-log", from: "1.5.3")
Expand All @@ -520,7 +531,6 @@ if swiftLogCondition {
openSwiftUITarget.addSwiftLogSettings()
}

let swiftCryptoCondition = envEnable("OPENSWIFTUI_SWIFT_CRYPTO", default: !buildForDarwinPlatform)
if swiftCryptoCondition {
package.dependencies.append(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.8.0")
Expand Down
282 changes: 278 additions & 4 deletions Sources/OpenSwiftUICore/Layout/Modifier/FrameLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// FrameLayout.swift
// OpenSwiftUICore
//
// Status: WIP
// Status: Complete
// ID: 73C64038119BBD0A6D8557B14379A404 (SwiftUICore)

public import Foundation
Expand Down Expand Up @@ -162,8 +162,7 @@ extension View {
/// - Returns: A view with fixed dimensions of `width` and `height`, for the
/// parameters that are non-`nil`.
@inlinable
nonisolated
public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View {
nonisolated public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View {
return modifier(
_FrameLayout(width: width, height: height, alignment: alignment)
)
Expand All @@ -182,4 +181,279 @@ extension View {
}
}

// MARK: - FlexFrameLayout [6.4.41] [WIP]
// MARK: - FlexFrameLayout [6.4.41]

/// A modifier that aligns its child in an invisible, flexible frame with size
/// limits and ideal size properties.
@frozen
public struct _FlexFrameLayout: UnaryLayout, FrameLayoutCommon {
let minWidth: CGFloat?
let idealWidth: CGFloat?
let maxWidth: CGFloat?
let minHeight: CGFloat?
let idealHeight: CGFloat?
let maxHeight: CGFloat?
let alignment: Alignment

/// Creates an instance with the given properties.
@usableFromInline
package init(
minWidth: CGFloat? = nil,
idealWidth: CGFloat? = nil,
maxWidth: CGFloat? = nil,
minHeight: CGFloat? = nil,
idealHeight: CGFloat? = nil,
maxHeight: CGFloat? = nil,
alignment: Alignment
) {
let minW: CGFloat? = if let minWidth {
max(minWidth, .zero)
} else {
nil
}
let ideaW: CGFloat? = if let idealWidth {
max(minW ?? .zero, idealWidth)
} else {
nil
}
let maxW: CGFloat? = if let maxWidth {
max(ideaW ?? .zero, maxWidth)
} else {
nil
}
let minH: CGFloat? = if let minHeight {
max(minHeight, .zero)
} else {
nil
}
let ideaH: CGFloat? = if let idealHeight {
max(minH ?? .zero, idealHeight)
} else {
nil
}
let maxH: CGFloat? = if let maxHeight {
max(ideaH ?? .zero, maxHeight)
} else {
nil
}
let hasInvalidWidth = (minWidth ?? .zero) > (idealWidth ?? maxWidth ?? .infinity) ||
(idealWidth ?? .zero) > (maxWidth ?? .infinity) ||
(minWidth ?? .zero).isInfinite || (minWidth ?? .zero).isNaN

let hasInvalidHeight = (minHeight ?? .zero) > (idealHeight ?? maxHeight ?? .infinity) ||
(idealHeight ?? 0.0) > (maxHeight ?? .infinity) ||
(minHeight ?? .zero).isInfinite || (minHeight ?? .zero).isNaN

if (hasInvalidWidth || hasInvalidHeight) && isLinkedOnOrAfter(.v2) {
Log.runtimeIssues("Invalid frame dimension (negative or non-finite).")
}

self.minWidth = minW
self.idealWidth = ideaW
self.maxWidth = maxW
self.minHeight = minH
self.idealHeight = ideaH
self.maxHeight = maxH
self.alignment = alignment
}

private func childProposal(myProposal: _ProposedSize) -> _ProposedSize {
let width: CGFloat? = if let idealWidth {
min(max(myProposal.width ?? idealWidth, minWidth ?? -.infinity), maxWidth ?? .infinity)
} else {
nil
}
let height: CGFloat? = if let idealHeight {
min(max(myProposal.height ?? idealHeight, minHeight ?? -.infinity), maxHeight ?? .infinity)
} else {
nil
}
return _ProposedSize(width: width, height: height)
}

package func sizeThatFits(
in proposedSize: _ProposedSize,
context: SizeAndSpacingContext,
child: LayoutProxy
) -> CGSize {
let width: CGFloat? = if let width = proposedSize.width {
if let minWidth, let maxWidth, minWidth <= maxWidth {
min(max(width, minWidth), maxWidth)
} else {
nil
}
} else {
idealWidth
}
let height: CGFloat? = if let height = proposedSize.height {
if let minHeight, let maxHeight, minHeight <= maxHeight {
min(max(height, minHeight), maxHeight)
} else {
nil
}
} else {
idealHeight
}
guard let width, let height else {
let childProposal = childProposal(myProposal: proposedSize)
let size = child.size(in: childProposal)

let finalWidth = if let width {
width
} else {
switch (minWidth, maxWidth) {
case let (minW?, maxW?) where minW <= maxW:
min(max(minW, size.width), maxW)
case let (minW?, nil):
max(min(childProposal.width ?? .infinity, size.width), minW)
case let (nil, maxW?):
min(max(childProposal.width ?? -.infinity, size.width), maxW)
default:
size.width
}
}
let finalHeight = if let height {
height
} else {
switch (minHeight, maxHeight) {
case let (minH?, maxH?) where minH <= maxH:
min(max(minH, size.height), maxH)
case let (minH?, nil):
max(min(childProposal.height ?? .infinity, size.height), minH)
case let (nil, maxH?):
min(max(childProposal.height ?? -.infinity, size.height), maxH)
default:
size.height
}
}
return CGSize(width: finalWidth, height: finalHeight)
}
return CGSize(width: width, height: height)
}

private func childPlacementProposal(of child: LayoutProxy, context: PlacementContext) -> _ProposedSize {
func proposedDimension(
_ axis: Axis,
min: CGFloat? = nil,
ideal: CGFloat? = nil,
max: CGFloat? = nil
) -> CGFloat? {
let value = context.size[axis]
guard ideal == nil,
context.proposedSize[axis] == nil,
(min ?? -.infinity) < value, value < (max ?? .infinity)
else {
return value
}
return nil
}
return _ProposedSize(
width: proposedDimension(.horizontal, min: minWidth, ideal: idealWidth, max: maxWidth),
height: proposedDimension(.vertical, min: minHeight, ideal: idealHeight, max: maxHeight)
)
}

package func placement(of child: LayoutProxy, in context: PlacementContext) -> _Placement {
let childProposal = if Semantics.FlexFrameIdealSizing.isEnabled {
childPlacementProposal(of: child, context: context)
} else {
_ProposedSize(context.size)
}
return commonPlacement(of: child, in: context, childProposal: childProposal)
}

package func spacing(in context: SizeAndSpacingContext, child: LayoutProxy) -> Spacing {
if _SemanticFeature_v3.isEnabled, !child.requiresSpacingProjection {
var spacing = child.layoutComputer.spacing()
var edges: Edge.Set = []
if minHeight != nil || idealHeight != nil || maxHeight != nil {
edges.formUnion(.vertical)
}
if minWidth != nil || idealWidth != nil || maxWidth != nil {
edges.formUnion(.horizontal)
}
spacing.reset(.init(edges, layoutDirection: context.layoutDirection))
return spacing
} else {
return child.layoutComputer.spacing()
}
}
}

extension View {
/// Positions this view within an invisible frame having the specified size
/// constraints.
///
/// Always specify at least one size characteristic when calling this
/// method. Pass `nil` or leave out a characteristic to indicate that the
/// frame should adopt this view's sizing behavior, constrained by the other
/// non-`nil` arguments.
///
/// The size proposed to this view is the size proposed to the frame,
/// limited by any constraints specified, and with any ideal dimensions
/// specified replacing any corresponding unspecified dimensions in the
/// proposal.
///
/// If no minimum or maximum constraint is specified in a given dimension,
/// the frame adopts the sizing behavior of its child in that dimension. If
/// both constraints are specified in a dimension, the frame unconditionally
/// adopts the size proposed for it, clamped to the constraints. Otherwise,
/// the size of the frame in either dimension is:
///
/// - If a minimum constraint is specified and the size proposed for the
/// frame by the parent is less than the size of this view, the proposed
/// size, clamped to that minimum.
/// - If a maximum constraint is specified and the size proposed for the
/// frame by the parent is greater than the size of this view, the
/// proposed size, clamped to that maximum.
/// - Otherwise, the size of this view.
///
/// - Parameters:
/// - minWidth: The minimum width of the resulting frame.
/// - idealWidth: The ideal width of the resulting frame.
/// - maxWidth: The maximum width of the resulting frame.
/// - minHeight: The minimum height of the resulting frame.
/// - idealHeight: The ideal height of the resulting frame.
/// - maxHeight: The maximum height of the resulting frame.
/// - alignment: The alignment of this view inside the resulting frame.
/// Note that most alignment values have no apparent effect when the
/// size of the frame happens to match that of this view.
///
/// - Returns: A view with flexible dimensions given by the call's non-`nil`
/// parameters.
@inlinable
nonisolated public func frame(
minWidth: CGFloat? = nil,
idealWidth: CGFloat? = nil,
maxWidth: CGFloat? = nil,
minHeight: CGFloat? = nil,
idealHeight: CGFloat? = nil,
maxHeight: CGFloat? = nil,
alignment: Alignment = .center
) -> some View {
func areInNondecreasingOrder(
_ min: CGFloat?, _ ideal: CGFloat?, _ max: CGFloat?
) -> Bool {
let min = min ?? -.infinity
let ideal = ideal ?? min
let max = max ?? ideal
return min <= ideal && ideal <= max
}

if !areInNondecreasingOrder(minWidth, idealWidth, maxWidth)
|| !areInNondecreasingOrder(minHeight, idealHeight, maxHeight)
{
Log.runtimeIssues("Contradictory frame constraints specified.")
}

return modifier(
_FlexFrameLayout(
minWidth: minWidth,
idealWidth: idealWidth, maxWidth: maxWidth,
minHeight: minHeight,
idealHeight: idealHeight, maxHeight: maxHeight,
alignment: alignment
)
)
}
}
8 changes: 8 additions & 0 deletions Sources/OpenSwiftUICore/Log/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
// Audited for iOS 18.0
// Status: Complete

#if DEBUG
import Foundation
#else
public import Foundation
#endif

#if OPENSWIFTUI_SWIFT_LOG
public import Logging
extension Logger {
Expand All @@ -19,6 +24,7 @@ extension Logger {
public import os.log

#if DEBUG
@usableFromInline
package let dso = { () -> UnsafeMutableRawPointer in
let count = _dyld_image_count()
for i in 0 ..< count {
Expand Down Expand Up @@ -110,6 +116,7 @@ package enum Log {
package static var runtimeIssuesLog = Logger(subsystem: "com.apple.runtime-issues", category: "OpenSwiftUI")

@_transparent
@usableFromInline
package static func runtimeIssues(
_ message: @autoclosure () -> StaticString,
_ args: @autoclosure () -> [CVarArg] = []
Expand All @@ -121,6 +128,7 @@ package enum Log {
package static var runtimeIssuesLog: OSLog = OSLog(subsystem: "com.apple.runtime-issues", category: "OpenSwiftUI")

@_transparent
@usableFromInline
package static func runtimeIssues(
_ message: @autoclosure () -> StaticString,
_ args: @autoclosure () -> [CVarArg] = []
Expand Down
Loading