Skip to content

Commit d7337f7

Browse files
authored
Add FlexFrameLayout support (#315)
* Add FlexFrameLayout support * Fix import issue
1 parent ab73864 commit d7337f7

File tree

9 files changed

+658
-18
lines changed

9 files changed

+658
-18
lines changed

Package.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ if linkCoreUI {
9797

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

100+
// MARK: - [env] OPENSWIFTUI_OPENCOMBINE
101+
102+
let openCombineCondition = envEnable("OPENSWIFTUI_OPENCOMBINE", default: !buildForDarwinPlatform)
103+
104+
// MARK: - [env] OPENSWIFTUI_SWIFT_LOG
105+
106+
let swiftLogCondition = envEnable("OPENSWIFTUI_SWIFT_LOG", default: !buildForDarwinPlatform)
107+
108+
// MARK: - [env] OPENSWIFTUI_SWIFT_CRYPTO
109+
110+
let swiftCryptoCondition = envEnable("OPENSWIFTUI_SWIFT_CRYPTO", default: !buildForDarwinPlatform)
111+
100112
// MARK: - [env] OPENGSWIFTUI_SWIFTUI_RENDER
101113

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

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

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

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

523-
let swiftCryptoCondition = envEnable("OPENSWIFTUI_SWIFT_CRYPTO", default: !buildForDarwinPlatform)
524534
if swiftCryptoCondition {
525535
package.dependencies.append(
526536
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.8.0")

Sources/OpenSwiftUICore/Layout/Modifier/FrameLayout.swift

Lines changed: 278 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// FrameLayout.swift
33
// OpenSwiftUICore
44
//
5-
// Status: WIP
5+
// Status: Complete
66
// ID: 73C64038119BBD0A6D8557B14379A404 (SwiftUICore)
77

88
public import Foundation
@@ -162,8 +162,7 @@ extension View {
162162
/// - Returns: A view with fixed dimensions of `width` and `height`, for the
163163
/// parameters that are non-`nil`.
164164
@inlinable
165-
nonisolated
166-
public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View {
165+
nonisolated public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View {
167166
return modifier(
168167
_FrameLayout(width: width, height: height, alignment: alignment)
169168
)
@@ -182,4 +181,279 @@ extension View {
182181
}
183182
}
184183

185-
// MARK: - FlexFrameLayout [6.4.41] [WIP]
184+
// MARK: - FlexFrameLayout [6.4.41]
185+
186+
/// A modifier that aligns its child in an invisible, flexible frame with size
187+
/// limits and ideal size properties.
188+
@frozen
189+
public struct _FlexFrameLayout: UnaryLayout, FrameLayoutCommon {
190+
let minWidth: CGFloat?
191+
let idealWidth: CGFloat?
192+
let maxWidth: CGFloat?
193+
let minHeight: CGFloat?
194+
let idealHeight: CGFloat?
195+
let maxHeight: CGFloat?
196+
let alignment: Alignment
197+
198+
/// Creates an instance with the given properties.
199+
@usableFromInline
200+
package init(
201+
minWidth: CGFloat? = nil,
202+
idealWidth: CGFloat? = nil,
203+
maxWidth: CGFloat? = nil,
204+
minHeight: CGFloat? = nil,
205+
idealHeight: CGFloat? = nil,
206+
maxHeight: CGFloat? = nil,
207+
alignment: Alignment
208+
) {
209+
let minW: CGFloat? = if let minWidth {
210+
max(minWidth, .zero)
211+
} else {
212+
nil
213+
}
214+
let ideaW: CGFloat? = if let idealWidth {
215+
max(minW ?? .zero, idealWidth)
216+
} else {
217+
nil
218+
}
219+
let maxW: CGFloat? = if let maxWidth {
220+
max(ideaW ?? .zero, maxWidth)
221+
} else {
222+
nil
223+
}
224+
let minH: CGFloat? = if let minHeight {
225+
max(minHeight, .zero)
226+
} else {
227+
nil
228+
}
229+
let ideaH: CGFloat? = if let idealHeight {
230+
max(minH ?? .zero, idealHeight)
231+
} else {
232+
nil
233+
}
234+
let maxH: CGFloat? = if let maxHeight {
235+
max(ideaH ?? .zero, maxHeight)
236+
} else {
237+
nil
238+
}
239+
let hasInvalidWidth = (minWidth ?? .zero) > (idealWidth ?? maxWidth ?? .infinity) ||
240+
(idealWidth ?? .zero) > (maxWidth ?? .infinity) ||
241+
(minWidth ?? .zero).isInfinite || (minWidth ?? .zero).isNaN
242+
243+
let hasInvalidHeight = (minHeight ?? .zero) > (idealHeight ?? maxHeight ?? .infinity) ||
244+
(idealHeight ?? 0.0) > (maxHeight ?? .infinity) ||
245+
(minHeight ?? .zero).isInfinite || (minHeight ?? .zero).isNaN
246+
247+
if (hasInvalidWidth || hasInvalidHeight) && isLinkedOnOrAfter(.v2) {
248+
Log.runtimeIssues("Invalid frame dimension (negative or non-finite).")
249+
}
250+
251+
self.minWidth = minW
252+
self.idealWidth = ideaW
253+
self.maxWidth = maxW
254+
self.minHeight = minH
255+
self.idealHeight = ideaH
256+
self.maxHeight = maxH
257+
self.alignment = alignment
258+
}
259+
260+
private func childProposal(myProposal: _ProposedSize) -> _ProposedSize {
261+
let width: CGFloat? = if let idealWidth {
262+
min(max(myProposal.width ?? idealWidth, minWidth ?? -.infinity), maxWidth ?? .infinity)
263+
} else {
264+
nil
265+
}
266+
let height: CGFloat? = if let idealHeight {
267+
min(max(myProposal.height ?? idealHeight, minHeight ?? -.infinity), maxHeight ?? .infinity)
268+
} else {
269+
nil
270+
}
271+
return _ProposedSize(width: width, height: height)
272+
}
273+
274+
package func sizeThatFits(
275+
in proposedSize: _ProposedSize,
276+
context: SizeAndSpacingContext,
277+
child: LayoutProxy
278+
) -> CGSize {
279+
let width: CGFloat? = if let width = proposedSize.width {
280+
if let minWidth, let maxWidth, minWidth <= maxWidth {
281+
min(max(width, minWidth), maxWidth)
282+
} else {
283+
nil
284+
}
285+
} else {
286+
idealWidth
287+
}
288+
let height: CGFloat? = if let height = proposedSize.height {
289+
if let minHeight, let maxHeight, minHeight <= maxHeight {
290+
min(max(height, minHeight), maxHeight)
291+
} else {
292+
nil
293+
}
294+
} else {
295+
idealHeight
296+
}
297+
guard let width, let height else {
298+
let childProposal = childProposal(myProposal: proposedSize)
299+
let size = child.size(in: childProposal)
300+
301+
let finalWidth = if let width {
302+
width
303+
} else {
304+
switch (minWidth, maxWidth) {
305+
case let (minW?, maxW?) where minW <= maxW:
306+
min(max(minW, size.width), maxW)
307+
case let (minW?, nil):
308+
max(min(childProposal.width ?? .infinity, size.width), minW)
309+
case let (nil, maxW?):
310+
min(max(childProposal.width ?? -.infinity, size.width), maxW)
311+
default:
312+
size.width
313+
}
314+
}
315+
let finalHeight = if let height {
316+
height
317+
} else {
318+
switch (minHeight, maxHeight) {
319+
case let (minH?, maxH?) where minH <= maxH:
320+
min(max(minH, size.height), maxH)
321+
case let (minH?, nil):
322+
max(min(childProposal.height ?? .infinity, size.height), minH)
323+
case let (nil, maxH?):
324+
min(max(childProposal.height ?? -.infinity, size.height), maxH)
325+
default:
326+
size.height
327+
}
328+
}
329+
return CGSize(width: finalWidth, height: finalHeight)
330+
}
331+
return CGSize(width: width, height: height)
332+
}
333+
334+
private func childPlacementProposal(of child: LayoutProxy, context: PlacementContext) -> _ProposedSize {
335+
func proposedDimension(
336+
_ axis: Axis,
337+
min: CGFloat? = nil,
338+
ideal: CGFloat? = nil,
339+
max: CGFloat? = nil
340+
) -> CGFloat? {
341+
let value = context.size[axis]
342+
guard ideal == nil,
343+
context.proposedSize[axis] == nil,
344+
(min ?? -.infinity) < value, value < (max ?? .infinity)
345+
else {
346+
return value
347+
}
348+
return nil
349+
}
350+
return _ProposedSize(
351+
width: proposedDimension(.horizontal, min: minWidth, ideal: idealWidth, max: maxWidth),
352+
height: proposedDimension(.vertical, min: minHeight, ideal: idealHeight, max: maxHeight)
353+
)
354+
}
355+
356+
package func placement(of child: LayoutProxy, in context: PlacementContext) -> _Placement {
357+
let childProposal = if Semantics.FlexFrameIdealSizing.isEnabled {
358+
childPlacementProposal(of: child, context: context)
359+
} else {
360+
_ProposedSize(context.size)
361+
}
362+
return commonPlacement(of: child, in: context, childProposal: childProposal)
363+
}
364+
365+
package func spacing(in context: SizeAndSpacingContext, child: LayoutProxy) -> Spacing {
366+
if _SemanticFeature_v3.isEnabled, !child.requiresSpacingProjection {
367+
var spacing = child.layoutComputer.spacing()
368+
var edges: Edge.Set = []
369+
if minHeight != nil || idealHeight != nil || maxHeight != nil {
370+
edges.formUnion(.vertical)
371+
}
372+
if minWidth != nil || idealWidth != nil || maxWidth != nil {
373+
edges.formUnion(.horizontal)
374+
}
375+
spacing.reset(.init(edges, layoutDirection: context.layoutDirection))
376+
return spacing
377+
} else {
378+
return child.layoutComputer.spacing()
379+
}
380+
}
381+
}
382+
383+
extension View {
384+
/// Positions this view within an invisible frame having the specified size
385+
/// constraints.
386+
///
387+
/// Always specify at least one size characteristic when calling this
388+
/// method. Pass `nil` or leave out a characteristic to indicate that the
389+
/// frame should adopt this view's sizing behavior, constrained by the other
390+
/// non-`nil` arguments.
391+
///
392+
/// The size proposed to this view is the size proposed to the frame,
393+
/// limited by any constraints specified, and with any ideal dimensions
394+
/// specified replacing any corresponding unspecified dimensions in the
395+
/// proposal.
396+
///
397+
/// If no minimum or maximum constraint is specified in a given dimension,
398+
/// the frame adopts the sizing behavior of its child in that dimension. If
399+
/// both constraints are specified in a dimension, the frame unconditionally
400+
/// adopts the size proposed for it, clamped to the constraints. Otherwise,
401+
/// the size of the frame in either dimension is:
402+
///
403+
/// - If a minimum constraint is specified and the size proposed for the
404+
/// frame by the parent is less than the size of this view, the proposed
405+
/// size, clamped to that minimum.
406+
/// - If a maximum constraint is specified and the size proposed for the
407+
/// frame by the parent is greater than the size of this view, the
408+
/// proposed size, clamped to that maximum.
409+
/// - Otherwise, the size of this view.
410+
///
411+
/// - Parameters:
412+
/// - minWidth: The minimum width of the resulting frame.
413+
/// - idealWidth: The ideal width of the resulting frame.
414+
/// - maxWidth: The maximum width of the resulting frame.
415+
/// - minHeight: The minimum height of the resulting frame.
416+
/// - idealHeight: The ideal height of the resulting frame.
417+
/// - maxHeight: The maximum height of the resulting frame.
418+
/// - alignment: The alignment of this view inside the resulting frame.
419+
/// Note that most alignment values have no apparent effect when the
420+
/// size of the frame happens to match that of this view.
421+
///
422+
/// - Returns: A view with flexible dimensions given by the call's non-`nil`
423+
/// parameters.
424+
@inlinable
425+
nonisolated public func frame(
426+
minWidth: CGFloat? = nil,
427+
idealWidth: CGFloat? = nil,
428+
maxWidth: CGFloat? = nil,
429+
minHeight: CGFloat? = nil,
430+
idealHeight: CGFloat? = nil,
431+
maxHeight: CGFloat? = nil,
432+
alignment: Alignment = .center
433+
) -> some View {
434+
func areInNondecreasingOrder(
435+
_ min: CGFloat?, _ ideal: CGFloat?, _ max: CGFloat?
436+
) -> Bool {
437+
let min = min ?? -.infinity
438+
let ideal = ideal ?? min
439+
let max = max ?? ideal
440+
return min <= ideal && ideal <= max
441+
}
442+
443+
if !areInNondecreasingOrder(minWidth, idealWidth, maxWidth)
444+
|| !areInNondecreasingOrder(minHeight, idealHeight, maxHeight)
445+
{
446+
Log.runtimeIssues("Contradictory frame constraints specified.")
447+
}
448+
449+
return modifier(
450+
_FlexFrameLayout(
451+
minWidth: minWidth,
452+
idealWidth: idealWidth, maxWidth: maxWidth,
453+
minHeight: minHeight,
454+
idealHeight: idealHeight, maxHeight: maxHeight,
455+
alignment: alignment
456+
)
457+
)
458+
}
459+
}

Sources/OpenSwiftUICore/Log/Logging.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
// Audited for iOS 18.0
66
// Status: Complete
77

8+
#if DEBUG
89
import Foundation
10+
#else
11+
public import Foundation
12+
#endif
13+
914
#if OPENSWIFTUI_SWIFT_LOG
1015
public import Logging
1116
extension Logger {
@@ -19,6 +24,7 @@ extension Logger {
1924
public import os.log
2025

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

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

123130
@_transparent
131+
@usableFromInline
124132
package static func runtimeIssues(
125133
_ message: @autoclosure () -> StaticString,
126134
_ args: @autoclosure () -> [CVarArg] = []

0 commit comments

Comments
 (0)