一组用于辅助布局的 SwiftUI 工具。
Layouts
,例如 HFlowLayout
、VFlowLayout
、VMasonryLayout
、HMasonryLayout
和 LayoutThatFits
AutoRotatingView
,用于设置视图允许的屏幕方向。WidthReader
、HeightReader
、onSizeChange(perform:)
、keyboardHeight
、.relativePadding
、ScaledView
和 OverlappingImage
。unclippedTextRenderer
,用于修复被裁剪的 Text
。SmartScrollView
,具有可选滚动、内容自适应框架和实时边缘内边距值。FlippingView
和 rotation3DEffect(back:)
,用于创建可翻转视图,并在背面显示不同的视图。TabMenu
,一个可自定义的 iOS 标签菜单,具有 onReselect
和 onDoubleTap
功能。一些与小组件相关的工具
AccessoryInlineImage
,用于在 accessoryInline
小组件内部使用任何图像WidgetSize
- 类似于 WidgetFamily,但按设备返回小组件框架尺寸,并且不需要 WidgetKit
WidgetDemoFrame
,创建精确尺寸的小组件框架,您可以在 iOS 或 macOS 应用程序中使用。适用于 iOS 14+15、macOS 11+12、watchOS 7+8 和 tvOS 14+15 的其他 SwiftUI 工具
FULayout
,用于构建自定义布局(类似于 SwiftUI Layout
)。HFlow
、VFlow
、HMasonry
、VMasonry
、FULayoutThatFits
和 FUViewThatFits
AnyFULayout
,用于包装多个布局并在它们之间进行动画切换。Custom FULayout
,并使用 LayoutFromFULayout
添加 SwiftUI Layout
版本TagView
,用于基于元素数组的简单流式视图。WidgetRelativeShape
,修复了 iPad 上 ContainerRelativeShape
的一个错误。Example
文件夹中有一个应用程序,演示了此软件包的功能。
此软件包兼容 iOS 14+、macOS 11+、watchOS 7+、tvOS 14+ 和 visionOS。
File -> Add Packages
https://github.com/ryanlintott/FrameUp
并按版本选择。import FrameUp
导入软件包实际上取决于您。我目前在我的 Old English Wordhord 应用程序 中使用了此软件包。
此外,如果您发现错误或想要新功能,请添加 issue,我会尽快回复您。
FrameUp 是开源且免费的,但如果您喜欢使用它,请考虑支持我的工作。
*iOS 16+、macOS 13+、watchOS 9+、tvOS 16+
如果您的目标操作系统较旧且不支持 SwiftUI Layout
,请使用 FULayout
等效项。
一个 Layout
,用于在水平行中排列视图,从一行流向下一行,具有可调整的水平和垂直间距,并支持水平和垂直对齐,包括一个对齐方式为 justified 的对齐,它将均匀地间隔已完成行中的元素。
每行的高度将由最高的元素决定。整体框架尺寸将适应布局内容的大小。
HFlowLayout {
ForEach(["Hello", "World", "More Text"], id: \.self) { item in
Text(item.value)
}
}
一个 Layout
,用于在垂直列中排列视图,从一列流向下一列,具有可调整的水平和垂直间距,并支持水平和垂直对齐,包括一个对齐方式为 justified 的对齐,它将均匀地间隔已完成列中的元素。
每列的宽度将由最宽的元素决定。整体框架尺寸将适应布局内容的大小。
VFlowLayout {
ForEach(["Hello", "World", "More Text"], id: \.self) { item in
Text(item.value)
}
}
一个 Layout
,通过将每个视图添加到最短的列中,将视图排列成设定数量的列。
VMasonryLayout(columns: 3) {
ForEach(["Hello", "World", "More Text"], id: \.self) { item in
Text(item.value)
}
}
一个 Layout
,通过将每个视图添加到最短的行中,将视图排列成设定数量的行。
HMasonryLayout(rows: 3) {
ForEach(["Hello", "World", "More Text"], id: \.self) { item in
Text(item.value)
}
}
使用首个适合所提供轴的布局,从布局偏好数组中创建布局。
LayoutThatFits(in: .horizontal, [HStackLayout(), VStackLayout()]) {
Color.green.frame(width: 50, height: 50)
Color.yellow.frame(width: 50, height: 200)
Color.blue.frame(width: 50, height: 100)
}
*仅限 iOS
一个视图,如果当前设备方向在允许的方向数组中,则将任何视图旋转以匹配当前设备方向。这对于允许全屏图像视图在仅限纵向模式的应用程序中使用横向模式最有用。它也可用于限制方向,例如在允许纵向模式的应用程序中仅限横向模式。旋转可以是动画的。
AutoRotatingView([.portrait, .landscapeLeft, .landscapeRight], animation: .default) {
Image("MyFullscreenImage")
.resizable()
.scaledToFit()
}
一个视图,它获取可用宽度并将此测量值提供给其内容。与“GeometryReader”不同,此视图不会占用所有可用高度,而是会适应内容的高度。
在垂直滚动视图中很有用,您希望在不指定框架高度的情况下测量宽度。
ScrollView {
WidthReader { width in
HStack(spacing: 0) {
Text("This text frame is set to 70% of the width.")
.frame(width: width * 0.7)
.background(Color.green)
Circle()
}
}
.foregroundColor(.white)
.background(Color.blue)
Text("The WidthReader above does not have a fixed height and will grow to fit the content.")
.padding()
}
一个视图,它获取可用高度并将此测量值提供给其内容。与“GeometryReader”不同,此视图不会占用所有可用宽度,而是会适应内容的宽度。
在水平滚动视图中很有用,您希望在不指定框架宽度的情况下测量高度。
ScrollView(.horizontal) {
HeightReader { height in
VStack(spacing: 0) {
Text("This\ntext\nframe\nis\nset\nto\n70%\nof\nthe\nheight.")
.frame(height: height * 0.7)
.background(Color.green)
Circle()
}
.foregroundColor(.white)
.background(Color.blue)
Text("\nThe\nHeightReader\nto\nthe\nleft\ndoes\nnot\nhave\na\nfixed\nwidth\nand\nwill\ngrow\nto\nfit\nthe\ncontent.")
.padding()
}
}
添加一个操作,以便在父视图尺寸值更改时执行。
struct OnSizeChangeExample: View {
@State private var size: CGSize = .zero
var body: some View {
Text("Hello, World!")
.padding(100)
.background(Color.blue)
.onSizeChange { size in
self.size = size
}
.overlay(Text("size: \(size.width) x \(size.height)"), alignment: .bottom)
}
}
在具有 .equalWidthContainer()
的视图中,具有 .equalWidthPreferred()
的视图将具有等于最大视图的宽度。如果空间有限,视图将平均缩小,每个视图缩小到适合内容的最小尺寸。它适用于 HStack
、VStack
甚至自定义布局,如 HFlow
当您希望窗口上的所有按钮都具有相同的宽度,同时仍然允许一些收缩时,这在 macOS 中尤其有效。
HStack {
Button { } label: {
Text("More Information")
.equalWidthPreferred()
}
Spacer()
Button { } label: {
Text("Cancel")
.equalWidthPreferred()
}
Spacer()
Button { } label: {
Text("OK")
.equalWidthPreferred()
}
}
.equalWidthContainer()
.frame(maxWidth: 400)
.padding()
在具有 .equalHeightContainer()
的视图中,具有 .equalHeightPreferred()
的视图将具有等于最大视图的高度。如果空间有限,视图将平均缩小,每个视图缩小到适合内容的最小尺寸。它适用于 HStack
、VStack
甚至自定义布局,如 VFlow
HStack {
Group {
Text("Here's something with some text")
Text("And more")
}
.foregroundColor(.white)
.padding()
.equalHeightPreferred()
.background(Color.blue.cornerRadius(10))
}
.equalHeightContainer()
.frame(maxWidth: 200)
一个环境变量,当 iOS 键盘出现和消失时,它将以动画方式更新。对于非 iOS 平台,它始终为零。
Animation.keyboard
作为键盘动画曲线的近似值添加,并由 keyboardHeight 使用。
为了使用 keyboardHeight,您首先需要将其添加到视图层次结构的顶部,以便它可以查看整个框架。它将在背景层上使用 GeometryReader 来测量键盘,因此请确保视图正在使用所有可用高度。
struct ContentView: View {
var body: some View {
MyView()
.frame(maxHeight: .infinity)
.keyboardHeightEnvironmentValue()
}
}
当您想要访问 keyboardHeight 时,请使用环境变量。如果您使用它来调整应避开键盘的视图的位置,请直接使用 keyboardHeight,并确保视图忽略键盘安全区域。
struct MyView: View {
@Environment(\.keyboardHeight) var keyboardHeight
@State private var text = ""
var body: some View {
TextField("Moves with keyboard", text: $text)
.keyboardHeightEnvironmentValue()
.padding(.bottom, keyboardHeight == 0 ? 100 : keyboardHeight)
.ignoresSafeArea(.keyboard)
}
}
向视图的指定边缘添加相对于视图大小的内边距量。宽度用于 .leading/.trailing,高度用于 .top/.bottom
负值可用于重叠内容。
Text("This text will have padding based on the width and height of its frame.")
.relativePadding([.leading, .top], 0.2)
一个视图修饰符,使用 scaleEffect
缩放视图以匹配框架尺寸。
视图必须具有固有内容尺寸或提供特定的框架尺寸。最终框架尺寸可能会因所选模式而异。
使用 ScaleMode 限制视图,使其只能增长/缩小或两者都可。
scaledToFrame(size:,contentMode:,scaleMode:)
scaledToFrame(width:,height:,contentMode:,scaleMode:)
scaledToFit(size:,scaleMode:)
scaledToFit(width:,height:,scaleMode:)
scaledToFit(width:,scaleMode:)
scaledToFit(height:,scaleMode:)
scaledToFill(size:,scaleMode:)
scaledToFill(width:,height:,scaleMode:)
一个图像视图,可以在其框架边缘重叠内容。
图像可以在垂直轴或水平轴上重叠,但不能同时在两个轴上重叠。
请务必考虑间距并使用 zIndex 将图像放置在内容的前面或后面。
VStack(spacing: 0) {
Text("Overlapping Image")
.font(.system(size: 50))
OverlappingImage(Image(systemName: "star.square"), aspectRatio: 1.0, top: 0.1, bottom: 0.25)
.padding(.horizontal, 50)
.zIndex(1)
Text("The image above will overlap content above and below.")
.padding(20)
}
*iOS 18+、macOS 15+、watchOS 11+、tvOS 18+、visionOS 2+
SwiftUI Text
有一个无法调整的裁剪框架,偶尔会裁剪渲染的文本。此修饰符应用一个 UnclippedTextRenderer
,它会移除此裁剪框架。
如果使用另一个文本渲染器,则此修饰符是不必要的,因为所有文本渲染器都会移除裁剪框架。
Text("f")
.font(.custom("zapfino", size: 30))
.unclippedTextRenderer()
*仅限 iOS
一个具有额外功能的 ScrollView。
SmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: true) {
// Content here
} onScroll: { edgeInsets in
// Runs when edge insets change
}
局限性
一个双面视图,可以通过点击或滑动翻转。
轴、锚点、透视、翻转的拖动距离、点击翻转的动画等都可以自定义。
对于 visionOS,可能需要稍微不同的初始化程序,并且翻转将在 3D 空间中发生。如果相反,您想要在平面视图上获得透视效果,则可以使用 PerspectiveFlippingView
FlippingView(flips: $flips) {
Color.blue.overlay(Text("Up"))
} back: {
Color.red.overlay(Text("Back"))
}
*在 visionOS 中已弃用 渲染视图的内容,就好像它围绕指定的轴在三维空间中旋转一样,并带有一个闭包,其中包含要在背面显示的不同的视图。
下面的示例是一个具有两面的视图。一面是蓝色,上面写着“Front”,背面是红色,上面写着“Back”。更改角度将显示每个面,因为它是可见的。
Color.blue.overlay(Text("Front"))
.rotation3DEffect(angle) {
Color.red.overlay(Text("Back"))
}
*visionOS 围绕给定的旋转轴在三维空间中旋转此视图的渲染输出,并带有一个闭包,其中包含背面不同的视图。需要最小厚度来偏移两个视图,以确保面向用户的面在顶部渲染。
Color.blue.overlay(Text("Front"))
.rotation3DEffect(angle) {
Color.red.overlay(Text("Back"))
}
*visionOS 渲染视图的内容,就好像它围绕指定的轴在三维空间中旋转一样,并带有一个闭包,其中包含要在背面显示的不同的视图。该视图实际上并未在 3D 空间中旋转。
Color.blue.overlay(Text("Front"))
.rotation3DEffect(angle) {
Color.red.overlay(Text("Back"))
}
*仅限 iOS
可自定义的标签菜单栏视图,旨在模仿 iPhone 上默认标签菜单栏的样式。提供的图像或视图和名称用于遮罩另一个提供的视图,这通常是一种颜色。
功能特性
let items = [
TabMenuItem(icon: AnyView(Circle().stroke().overlay(Text("i"))), name: "Info", tab: 0),
TabMenuItem(image: Image(systemName: "star"), name: "Favourites", tab: 1),
TabMenuItem(image: Image(systemName: "bookmark"), name: "Categories", tab: 2),
TabMenuItem(image: Image(systemName: "books.vertical"), name: "About", tab: 3)
]
TabMenuView(selection: $selection, items: items) { isSelected in
Group {
if isSelected {
Color.accentColor
} else {
Color(.secondaryLabel)
}
}
} onReselect: {
NamedAction("Reselect") {
print("TabMenu item \(selection) reselected")
}
} onDoubleTap: {
NamedAction("Double Tap") {
print("TabMenu item \(selection) doubletapped")
}
}
一个图像,将被缩放并调整渲染模式以在 accessoryInline
小组件内部工作。图像将缩放以适应框架并应用模板渲染模式。
在 Label 的 icon 属性内部使用。
Label {
Text("Label Text")
} icon: {
AccessoryInlineImage("myImage")
}
一个类似于 WidgetFamily 的枚举,但按设备返回小组件框架尺寸,并且不需要 WidgetKit
,因此可以在您的主 iOS 或 macOS 应用程序内部使用。
根据提供的屏幕尺寸返回小组件的尺寸。
根据提供的屏幕尺寸,返回小组件的设计画布或主屏幕尺寸(取决于提供的目标)。在 iPad 上,小组件内容被放置在设计画布上,然后缩放以适应主屏幕尺寸。(WidgetDemoFrame
将为您执行此缩放)
根据设备类型和 iOS 版本返回支持的小组件尺寸数组。
根据当前设备返回小组件的尺寸。
所有小组件尺寸信息均来自:Apple - 人机界面指南
创建针对提供的屏幕尺寸或当前设备(仅限 iOS)调整大小的小组件框架。用于从应用程序内部显示示例小组件。
圆角尺寸默认为 20,可能与实际小组件圆角半径不同。
对于 iPad,小组件视图使用设计尺寸,并使用 ScaledView
缩放到较小的主屏幕尺寸。此演示框架使用相同的缩放来正确预览小组件。所有尺寸都适用于所有设备和所有版本的 iOS(甚至 iOS 14.0 上的 iPhone 上的 extraLarge)。
WidgetDemoFrame(.medium, cornerRadius: 20) { size, cornerRadius in
Text("Demo Widget")
}
一个协议,添加了有用的参数,如 aspectFormat
、aspectRatio
、minDimension
和 maxDimension
。
用于具有 width
和 height
属性的类型,如 CGSize
。
如何在您的应用程序中添加一致性
extension CGSize: Proportionable { }
frame(width:,height:,alignment:)
View 修饰符的替代方案,它接受 CGSize
参数。
如果您喜欢 SwiftUI Layout
协议,但您需要以不支持它的旧操作系统为目标,那么 FULayout
协议可能是您的答案!
FULayout
的工作方式与 SwiftUI Layout
相同。主要区别在于,在初始化时,它将需要 maxWidth
或 maxHeight
参数,以便了解可用空间。这可以通过 GeometryReader
或此软件包中的 WidthReader
或 HeightReader
提供。
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 FULayout
将 callAsFunction()
与视图构建器一起使用,因此您可以像使用 SwiftUI Layout
一样使用它。
VFlow(maxWidth: 200) {
Text("Hello")
Text("World")
}
注意:此方法在底层使用了 Apple 的私有协议 _VariadicView
。Apple 可能会更改实现,这存在很小的风险,因此如果这让您担心,请使用下面的方法 2。
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 此方法的工作方式与 ForEach()
非常相似。
MyFULayout().forEach(["Hello", "World"], id: \.self) { item in
Text(item.value)
}
}
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 FULayout
等效于 HFlowLayout
。
一个 FrameUp FULayout
,用于在水平行中排列视图,从一行流向下一行,具有可调整的水平和垂直间距,并支持水平和垂直对齐,包括一个对齐方式为 justified 的对齐,它将均匀地间隔已完成行中的元素。
每行的高度将由该行中最高的视图决定。
WidthReader { width in
HFlow(maxWidth: width) {
ForEach(["Hello", "World", "More Text"], id: \.self) { item in
Text(item.value)
}
}
}
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 FULayout
等效于 VFlowLayout
。
一个 FrameUp FULayout
,用于在垂直列中排列视图,从一列流向下一列,具有可调整的水平和垂直间距,并支持水平和垂直对齐,包括一个对齐方式为 justified 的对齐,它将均匀地间隔已完成列中的元素。
每列的宽度将由最宽的元素决定。
WidthReader { width in
VFlow(maxWidth: width) {
ForEach(["Hello", "World", "More Text"], id: \.self) { item in
Text(item.value)
}
}
}
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 FULayout
等效于 HMasonryLayout
。
一个 FrameUp FULayout
,通过将每个视图添加到最短的行中,将视图排列成设定数量的行。
HeightReader { height in
HMasonry(columns: 3, maxHeight: height) {
ForEach(["Hello", "World", "More Text"], id: \.self) { item in
Text(item.value)
.frame(maxHeight: .infinity, alignment: .center)
}
}
}
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 FULayout
等效于 VMasonryLayout
。
一个 FrameUp FULayout
,通过将每个视图添加到最短的行中,将视图排列成设定数量的行。
WidthReader { width in
VMasonry(columns: 3, maxWidth: width) {
ForEach(["Hello", "World", "More Text"], id: \.self) { item in
Text(item.value)
.frame(maxWidth: .infinity, alignment: .center)
}
}
}
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 FULayout
等效于 LayoutThatFits
。
一个 FULayout
,它选择第一个提供的、适合提供的 maxWidth、maxHeight 或两者的布局。当在 HStackFULayout
和 VStackFULayout
之间切换时,这最有帮助,因为内容只需要提供一次,甚至在堆栈更改时也会进行动画处理。
FULayoutThatFits(
maxWidth: maxWidth,
layouts: [
HStackFULayout(maxHeight: 1000),
VStackFULayout(maxWidth: maxWidth)
]
) {
Color.green.frame(width: 50, height: 50)
Color.yellow.frame(width: 50, height: 200)
Color.blue.frame(width: 50, height: 100)
}
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 FULayout
等效于 SwiftUI ViewThatFits
。
一个 FULayout
,它呈现第一个适合提供的 maxWidth、maxHeight 或两者的视图,具体取决于使用了哪些参数。
由于此视图无法测量可用空间,因此需要使用 GeometryReader
、WidthReader
或 HeightReader
传入 maxWidth 和/或 maxHeight 参数。
WidthReader { width in
FUViewThatFits(maxWidth: width) {
Group {
Text("This layout will pick the first view that fits the available width.")
Text("Maybe this?")
Text("OK!")
}
.fixedSize(horizontal: true, vertical: false)
}
}
(在此示例中需要使用 .fixedSize
,否则第一个视图将通过截断文本自动适应)
可以包装在 AnyFULayout
中的替代堆栈布局,然后在它们之间进行动画切换。当您想要根据可用空间在 VStack 和 HStack 之间切换时很有用。
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 类似于 HStack
,但不能使用 Spacer()
,并且内容将始终在水平轴上使用固定尺寸。
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 类似于 VStack
,但不能使用 Spacer()
,并且内容将始终在垂直轴上使用固定尺寸。
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 类似于 ZStack
,但内容将始终在垂直和水平轴上使用固定尺寸。
*已弃用 iOS 16、macOS 13、watchOS 7、tvOS 14、visionOS 1 FULayout
等效于 SwiftUI AnyLayout
。
一个类型擦除的 FrameUp 布局,可用于包装多个布局并在它们之间进行动画切换。
struct AnyFULayoutSimple: View {
let isVStack: Bool
let maxSize: CGSize
var layout: any FULayout {
isVStack ? VStackFULayout(maxWidth: maxSize.width) : HStackFULayout(maxHeight: maxSize.height)
}
var body: some View {
AnyFULayout(layout) {
Text("First")
Text("Second")
Text("Third")
}
.animation(.spring(), value: isVStack)
}
}
FrameUp FULayout
协议要求您定义哪些轴是固定的、最大项目尺寸以及一个接受视图尺寸并输出视图偏移量的函数。
下面是一个示例布局,它在中心线的左右两侧排列视图。
struct CustomFULayout: FULayout {
/// Add parameters here to adjust layout
/// Define these required parameters
var fixedSize: Axis.Set = .horizontal
var maxItemWidth: CGFloat? { maxWidth }
var maxItemHeight: CGFloat? = nil
func contentOffsets(sizes: [Int : CGSize]) -> [Int : CGPoint] {
/// Write code that uses the dictionary of sizes and your parameters to output a dictionary of offsets from the top left corner.
}
}
如果您创建了 FULayout
,则可以使用它轻松创建 SwiftUI Layout
。
struct CustomLayout: LayoutFromtFULayout {
/// Add parameters here to adjust layout
/// Add this function that will create the associated FULayout
func fuLayout(maxSize: CGSize) -> CustomFULayout {
CustomFULayout(
/// Pass parameters through to FULayout using maxSize to help define the maximum item size.
)
}
}
一个较旧且更简单的 HFlow
版本,并非基于 FULayout
。
一个视图,它基于元素数组从左到右创建视图,并在需要时添加行。每行的高度将由最高的元素决定。
警告:在 ScrollView 中不起作用。
TagView(elements: ["One", "Two", "Three"]) { element in
Text(element)
}
一个视图,它基于元素数组从左到右创建视图,并在需要时添加行。每行的高度将由最高的元素决定。
必须提供最大宽度,但可以使用 WidthReader
获取该值。
WidthReader { width in
TagView(maxWidth: width, elements: ["One", "Two", "Three"]) { element in
Text(element)
}
}
*仅限 iOS
ContainerRelativeShape
的重新缩放版本,用于修复在运行 iOS 15 及更早版本的 iPad 上圆角半径的一个错误。对于 iOS 16 及更高版本,它的作用与 ContainerRelativeShape 完全相同。
此示例具有蓝色背景,并带有 1 个点的内边距。在运行 iOS 15 或更早版本的 iPad 上,红色背景将显示在角上,因为圆角半径不匹配。
Text("Example widget")
.background(.blue)
.clipShape(WidgetRelativeShape(.systemSmall))
.background(
ContainerRelativeShape()
.fill(.red)
)
.padding(1)