{"id":19093058,"url":"https://github.com/ryanlintott/frameup","last_synced_at":"2025-04-12T23:40:00.074Z","repository":{"id":40631272,"uuid":"406496971","full_name":"ryanlintott/FrameUp","owner":"ryanlintott","description":"Reframing SwiftUI Views. A collection of tools to help with layout.","archived":false,"fork":false,"pushed_at":"2025-04-02T13:50:37.000Z","size":1365,"stargazers_count":288,"open_issues_count":2,"forks_count":6,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-12T23:39:55.047Z","etag":null,"topics":["layout","swift","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ryanlintott.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-14T19:29:58.000Z","updated_at":"2025-04-05T09:07:33.000Z","dependencies_parsed_at":"2023-12-05T23:23:25.297Z","dependency_job_id":"b451c9f0-8f1f-46e4-8866-c0fb729c44b9","html_url":"https://github.com/ryanlintott/FrameUp","commit_stats":{"total_commits":95,"total_committers":2,"mean_commits":47.5,"dds":"0.17894736842105263","last_synced_commit":"41a53641d5958738c77241766d639ac34bffb1c2"},"previous_names":[],"tags_count":35,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FFrameUp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FFrameUp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FFrameUp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FFrameUp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryanlintott","download_url":"https://codeload.github.com/ryanlintott/FrameUp/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248647254,"owners_count":21139081,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["layout","swift","swiftui"],"created_at":"2024-11-09T03:23:11.024Z","updated_at":"2025-04-12T23:40:00.046Z","avatar_url":"https://github.com/ryanlintott.png","language":"Swift","funding_links":["https://ko-fi.com/X7X04PU6T"],"categories":[],"sub_categories":[],"readme":"\u003cimg width=\"456\" alt=\"FrameUp Logo\" src=\"https://user-images.githubusercontent.com/2143656/149010960-2b0e1200-b6d4-40a5-bbe7-4aabc5ce6b09.png\"\u003e\n\n[![Swift Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fryanlintott%2FFrameUp%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ryanlintott/FrameUp)\n[![Platform Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fryanlintott%2FFrameUp%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ryanlintott/FrameUp)\n![License - MIT](https://img.shields.io/github/license/ryanlintott/FrameUp)\n![Version](https://img.shields.io/github/v/tag/ryanlintott/FrameUp?label=version)\n![GitHub last commit](https://img.shields.io/github/last-commit/ryanlintott/FrameUp)\n[![Mastodon](https://img.shields.io/badge/mastodon-@ryanlintott-5c4ee4.svg?style=flat)](http://mastodon.social/@ryanlintott)\n[![Twitter](https://img.shields.io/badge/twitter-@ryanlintott-blue.svg?style=flat)](http://twitter.com/ryanlintott)\n\n# Overview\nA collection of SwiftUI tools to help with layout.\n\n- SwiftUI [`Layouts`](#layouts) like [`HFlowLayout`](#hflowlayout), [`VFlowLayout`](#vflowlayout), [`VMasonryLayout`](#vmasonrylayout), [`HMasonryLayout`](#hmasonrylayout), and [`LayoutThatFits`](#layoutthatfits)\n- [`AutoRotatingView`](#autorotatingview) to set allowable orientations for a view.\n- [Frame Adjustment](#frame-adjustment) tools like [`WidthReader`](#widthreader), [`HeightReader`](#heightreader), [`onSizeChange(perform:)`](#onsizechangeperform), [`keyboardHeight`](#keyboardHeight), [`.relativePadding`](#relativepaddingedges-lengthfactor), [`ScaledView`](#scaledview) and [`OverlappingImage`](#overlappingimage).\n- [`unclippedTextRenderer`](#unclippedtextrenderer) for fixing clipped `Text`. \n- [`SmartScrollView`](#smartscrollview) with optional scrolling, a content-fitable frame, and live edge inset values.\n- [`FlippingView`](#flippingview) and [`rotation3DEffect(back:)`](#rotation3deffectback) for making flippable views with a different view on the back side.\n- [`TabMenu`](#tabmenu), a customizable iOS tab menu with `onReselect` and `onDoubleTap` functions.\n\nSome widget-related tools\n\n- [`AccessoryInlineImage`](#accessoryinlineimage) to use any image inside an `accessoryInline` widget\n- [`WidgetSize`](#widgetsize) - Similar to WidgetFamily but returns widget frame sizes by device and doesn't require `WidgetKit`\n- [`WidgetDemoFrame`](#widgetdemoframe) creates accurately sized widget frames you can use in an iOS or macOS app.\n\nAdditional SwiftUI tools for iOS 14+15, macOS 11+12, watchOS 7+8, and tvOS 14+15\n\n- [`FULayout`](#fulayout) for building custom layouts (similar to SwiftUI `Layout`).\n- FULayouts: [`HFlow`](#hflow), [`VFlow`](#vflow), [`HMasonry`](#hmasonry), [`VMasonry`](#vmasonry), [`FULayoutThatFits`](#fulayoutthatfits), and [`FUViewThatFits`](#fuviewthatfits)\n- [`AnyFULayout`](#anyfulayout) to wrap multiple layouts and switch between with animation.\n- Make your own [`Custom FULayout`](#custom-fulayout) and add a SwiftUI `Layout` version using [`LayoutFromFULayout`](#layoutfromfulayout)\n- [`TagView`](#tagview) for a simple flow view based on an array of elements.\n- [`WidgetRelativeShape`](#widgetrelativeshape) fixes a `ContainerRelativeShape` bug on iPad.\n\n\n# Demo App\nThe `Example` folder has an app that demonstrates the features of this package.\n\n# Installation and Usage\nThis package is compatible with iOS 14+, macOS 11+, watchOS 7+, tvOS 14+, and visionOS.\n\n1. In Xcode go to `File -\u003e Add Packages`\n2. Paste in the repo's url: `https://github.com/ryanlintott/FrameUp` and select by version.\n3. Import the package using `import FrameUp`\n\n## Is it Production-Ready?\nReally it's up to you. I currently use this package in my own [Old English Wordhord app](https://oldenglishwordhord.com/app).\n\nAdditionally, if you find a bug or want a new feature add an issue and I will get back to you about it.\n\n# Support This Project\nFrameUp is open source and free but if you like using it, please consider supporting my work.\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/X7X04PU6T)\n\n- - -\n# Features\n\n## Layouts\n*\\*iOS 16+, macOS 13+, watchOS 9+, tvOS 16+*\n\nIf your target OS is older and doesn't support SwiftUI `Layout`, use [`FULayout`](#fulayout) equivalents.\n\n### HFlowLayout\nA `Layout` that arranges views in horizontal rows flowing from one to the next with adjustable horizontal and vertical spacing and support for horiztonal and vertical alignment including a justified alignment that will space elements in completed rows evenly.\n\nEach row height will be determined by the tallest element. The overall frame size will fit to the size of the laid out content.\n\n```swift\nHFlowLayout {\n    ForEach([\"Hello\", \"World\", \"More Text\"], id: \\.self) { item in\n        Text(item.value)\n    }\n}\n```\n\n### VFlowLayout\nA `Layout` that arranges views in vertical columns flowing from one to the next with adjustable horizontal and vertical spacing and support for horiztonal and vertical alignment including a justified alignment that will space elements in completed columns evenly.\n\nEach column width will be determined by the widest element. The overall frame size will fit to the size of the laid out content.\n\n```swift\nVFlowLayout {\n    ForEach([\"Hello\", \"World\", \"More Text\"], id: \\.self) { item in\n        Text(item.value)\n    }\n}\n```\n\n### VMasonryLayout\nA `Layout` that arranges views into a set number of columns by adding each view to the shortest column.\n\n```swift\nVMasonryLayout(columns: 3) {\n    ForEach([\"Hello\", \"World\", \"More Text\"], id: \\.self) { item in\n        Text(item.value)\n    }\n}\n```\n \n### HMasonryLayout\nA `Layout` that arranges views into a set number of rows by adding each view to the shortest row.\n\n```swift\nHMasonryLayout(rows: 3) {\n    ForEach([\"Hello\", \"World\", \"More Text\"], id: \\.self) { item in\n        Text(item.value)\n    }\n}\n```\n\n### LayoutThatFits\nCreates a layout using the first layout that fits in the axes provided from the array of layout preferences.\n\n```swift\nLayoutThatFits(in: .horizontal, [HStackLayout(), VStackLayout()]) {\n    Color.green.frame(width: 50, height: 50)\n    Color.yellow.frame(width: 50, height: 200)\n    Color.blue.frame(width: 50, height: 100)\n}\n```\n\n## AutoRotatingView\n*\\*iOS only*\n\nA view that rotates any view to match the current device orientation if it's in an array of allowed orientations. This is most useful for allowing fullscreen image views to use landscape orientations while inside a portrait-only app. It can also be used to limit orientations such as landscape-only in an app that allows portrait. Rotations can be animated.\n\n```swift\nAutoRotatingView([.portrait, .landscapeLeft, .landscapeRight], animation: .default) {\n    Image(\"MyFullscreenImage\")\n        .resizable()\n        .scaledToFit()\n}\n```\n\n## Frame Adjustment\n### WidthReader\nA view that takes the available width and provides this measurement to its content. Unlike 'GeometryReader' this view will not take up all the available height and will instead fit the height of the content.\n\nUseful inside vertical scroll views where you want to measure the width without specifying a frame height.\n\n```swift\nScrollView {\n    WidthReader { width in\n        HStack(spacing: 0) {\n            Text(\"This text frame is set to 70% of the width.\")\n                .frame(width: width * 0.7)\n                .background(Color.green)\n\n            Circle()\n        }\n    }\n    .foregroundColor(.white)\n    .background(Color.blue)\n\n    Text(\"The WidthReader above does not have a fixed height and will grow to fit the content.\")\n        .padding()\n}\n```\n\n### HeightReader\nA view that takes the available height and provides this measurement to its content. Unlike 'GeometryReader' this view will not take up all the available width and will instead fit the width of the content.\n\nUseful inside horizontal scroll views where you want to measure the height without specifying a frame width.\n\n```swift\nScrollView(.horizontal) {\n    HeightReader { height in\n        VStack(spacing: 0) {\n            Text(\"This\\ntext\\nframe\\nis\\nset\\nto\\n70%\\nof\\nthe\\nheight.\")\n                .frame(height: height * 0.7)\n                .background(Color.green)\n\n            Circle()\n        }\n        .foregroundColor(.white)\n        .background(Color.blue)\n\n        Text(\"\\nThe\\nHeightReader\\nto\\nthe\\nleft\\ndoes\\nnot\\nhave\\na\\nfixed\\nwidth\\nand\\nwill\\ngrow\\nto\\nfit\\nthe\\ncontent.\")\n            .padding()\n    }\n}\n```\n\n### .onSizeChange(perform:)\nAdds an action to perform when parent view size value changes.\n\n```swift\nstruct OnSizeChangeExample: View {\n    @State private var size: CGSize = .zero\n    \n    var body: some View {\n        Text(\"Hello, World!\")\n            .padding(100)\n            .background(Color.blue)\n            .onSizeChange { size in\n                self.size = size\n            }\n            .overlay(Text(\"size: \\(size.width) x \\(size.height)\"), alignment: .bottom)\n    }\n}\n```\n\n### equalWidthPreferred\nViews with `.equalWidthPreferred()` inside a view with `.equalWidthContainer()` will have widths equal to the largest view. If space is limited views will shrink equally, each to a minimum size that fits the content. It works in `HStack`, `VStack` and even custom layouts like `HFlow`\n\nThis works especially well in macOS when you want all buttons on a window to have the same width while still allowing some shrinking.\n\n```swift\nHStack {\n    Button { } label: {\n        Text(\"More Information\")\n            .equalWidthPreferred()\n    }\n    Spacer()\n    \n    Button { } label: {\n        Text(\"Cancel\")\n            .equalWidthPreferred()\n    }\n    \n    Spacer()\n    \n    Button { } label: {\n        Text(\"OK\")\n            .equalWidthPreferred()\n    }\n}\n.equalWidthContainer()\n.frame(maxWidth: 400)\n.padding()\n```\n\n### equalHeightPreferred\nViews with `.equalHeightPreferred()` inside a view with `.equalHeightContainer()` will have heights equal to the largest view. If space is limited views will shrink equally, each to a minimum size that fits the content. It works in `HStack`, `VStack` and even custom layouts like `VFlow`\n\n```swift\nHStack {\n    Group {\n        Text(\"Here's something with some text\")\n        \n        Text(\"And more\")\n    }\n    .foregroundColor(.white)\n    .padding()\n    .equalHeightPreferred()\n    .background(Color.blue.cornerRadius(10))\n}\n.equalHeightContainer()\n.frame(maxWidth: 200)\n```\n\n### keyboardHeight\nAn environment variable that will update with animation as the iOS keyboard appears and disappears. It will always be zero for non-iOS platforms. \n\n`Animation.keyboard` is added as an approximation of the keyboard animation curve and is used by keyboardHeight.\n\nIn order to use keyboardHeight you first need to add it somewhere at the top of your view heirachry so it can see the entire frame. It will use a GeometryReader on a background layer to measure the keyboard so ensure the view is using the entire available height.\n\n```swift\nstruct ContentView: View {\n    var body: some View {\n        MyView()\n            .frame(maxHeight: .infinity)\n            .keyboardHeightEnvironmentValue()\n    }\n}\n```\n\nWhen you want to access the keyboardHeight use an environment variable. If you're using it to adjust the position of a view that should avoid the keyboard use the keyboardHeight directly and make sure the view ignores the keyboard safe area.\n\n```swift\nstruct MyView: View {\n    @Environment(\\.keyboardHeight) var keyboardHeight\n    @State private var text = \"\"\n    \n    var body: some View {\n        TextField(\"Moves with keyboard\", text: $text)\n            .keyboardHeightEnvironmentValue()\n            .padding(.bottom, keyboardHeight == 0 ? 100 : keyboardHeight)\n            .ignoresSafeArea(.keyboard)\n    }\n}\n```\n\n### .relativePadding(edges:, lengthFactor:)\nAdds a padding amount to specified edges of a view relative to the size of the view. Width is used for .leading/.trailing and height is used for .top/.bottom\n\nNegative values can be used to overlap content.\n\n```swift\nText(\"This text will have padding based on the width and height of its frame.\")\n    .relativePadding([.leading, .top], 0.2)\n```\n\n### ScaledView\nA view modifier that scales a view using `scaleEffect` to match a frame size.\n\nView must have an intrinsic content size or be provided a specific frame size. Final frame size may be different depending on modes chosen.\n\nUses ScaleMode to limit the view so it can only grow/shrink or both.\n\n### Used in these view Extensions\n- `scaledToFrame(size:,contentMode:,scaleMode:)`\n- `scaledToFrame(width:,height:,contentMode:,scaleMode:)`\n- `scaledToFit(size:,scaleMode:)`\n- `scaledToFit(width:,height:,scaleMode:)`\n- `scaledToFit(width:,scaleMode:)`\n- `scaledToFit(height:,scaleMode:)`\n- `scaledToFill(size:,scaleMode:)`\n- `scaledToFill(width:,height:,scaleMode:)`\n\n### OverlappingImage\nAn image view that can overlap content on the edges of its frame.\n\nImage can overlap either on the vertical or horizontal axis but not both.\n\nBe sure to consider spacing and use zIndex to place the image in front or behind content.\n\n```swift\nVStack(spacing: 0) {\n    Text(\"Overlapping Image\")\n        .font(.system(size: 50))\n\n    OverlappingImage(Image(systemName: \"star.square\"), aspectRatio: 1.0, top: 0.1, bottom: 0.25)\n        .padding(.horizontal, 50)\n        .zIndex(1)\n\n    Text(\"The image above will overlap content above and below.\")\n        .padding(20)\n}\n```\n\n## Text\n### unclippedTextRenderer\n*\\*iOS 18+, macOS 15+, watchOS 11+, tvOS 18+, visionOS 2+*\n\nSwiftUI `Text` has a clipping frame that cannot be adjusted and will occasionally clip the rendered text. This modifier applies an `UnclippedTextRenderer` that removes this clipping frame.\n\nThis modifier is unnecessary if another text renderer is used as all text renderers will remove the clipping frame.\n\n```swift\nText(\"f\")\n    .font(.custom(\"zapfino\", size: 30))\n    .unclippedTextRenderer()\n```\n\n## SmartScrollView\n*\\*iOS only*\n\nA ScrollView with extra features.\n- Optional Scrolling - When active, the view will only be scrollable if the content is too large to fit in the parent frame. Enabled by default.\n- Shrink to Fit - When active, the view will only take as much vertical and horizontal space as is required to fit the content. Enabled by default.\n- Edge Insets - An onScroll function runs when the edge insets update. This occurs on scroll, on first load and on any size change to the scroll view or the content. Insets are negative when content edges are beyond the scroll view edges. Values may not be exactly 0 but will be less than 1 when content edges match scroll view edges.\n\n```swift\nSmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: true) {\n    // Content here\n} onScroll: { edgeInsets in\n    // Runs when edge insets change\n}\n```\n\n**Limitations:**\n- If placed directly inside a NavigationView with a resizing header, this view may behave strangely when scrolling. To avoid this add 1 point of padding just inside the NavigationView.\n- If the available space for this view grows for any reason other than screen rotation, this view might not grow to fill the space.\n\n## FlippingView\n\n### FlippingView\nA two-sided view that can be flipped by tapping or swiping.\n\nThe axis, anchor, perspective, drag distance to flip, animation for tap to flip and more can all be customized.\n\nFor visionOS a slightly different initializer might be needed and the flips will occur in 3d space. If instead you want a perspective effect on a flat view you can use `PerspectiveFlippingView`\n\n```swift\nFlippingView(flips: $flips) {\n    Color.blue.overlay(Text(\"Up\"))\n} back: {\n    Color.red.overlay(Text(\"Back\"))\n}\n```\n\n### rotation3DEffect(angle:axis:anchor:anchorZ:perspective:backsideFlip:back:)\n*\\*deprecated in visionOS*\nRenders a view’s content as if it’s rotated in three dimensions around the specified axis with a closure containing a different view to show on the back.\n\nThe example below is a view with two sides. One blue side that says \"Front\" and a red side on the back that says \"Back\". Changing the angle will show each side as it becomes visible.\n\n```swift\nColor.blue.overlay(Text(\"Front\"))\n    .rotation3DEffect(angle) {\n        Color.red.overlay(Text(\"Back\"))\n    }\n```\n\n### rotation3DEffect(angle:axis:anchor:backsideFlip:back:)\n*\\*visionOS*\nRotates this view’s rendered output in three dimensions around the given axis of rotation with a closure containing a different view on the back. A minimum thickness that offsets the two views is required to ensure the side facing the user renders on top.\n```swift\nColor.blue.overlay(Text(\"Front\"))\n    .rotation3DEffect(angle) {\n        Color.red.overlay(Text(\"Back\"))\n    }\n```\n\n### perspectiveRotationEffect(angle:axis:anchor:anchorZ:perspective:backsideFlip:back:)\n*\\*visionOS*\nRenders a view’s content as if it’s rotated in three dimensions around the specified axis with a closure containing a different view to show on the back. The view is not actually rotated in 3d space.\n\n```swift\nColor.blue.overlay(Text(\"Front\"))\n    .rotation3DEffect(angle) {\n        Color.red.overlay(Text(\"Back\"))\n    }\n```\n\n## TabMenu\n*\\*iOS only*\n\nCustomizable tab menu bar view designed to mimic the style of the default tab menu bar on iPhone. Images or views and name provided are used to mask another provided view which is often a color.\n\nFeatures:\n- Use any image or AnyView as a mask for the menu item.\n- Use any view as the 'color' including gradients.\n- onReselect closure that returns a NamedAction that triggers when the active tab is selected.\n- onDoubleTap closure that returns a NamedAction that triggers when the active tab is double-tapped.\n- accessibility actions are automatically added for onReselect and onDoubleTap if they are added.\n\n```swift\nlet items = [\n    TabMenuItem(icon: AnyView(Circle().stroke().overlay(Text(\"i\"))), name: \"Info\", tab: 0),\n    TabMenuItem(image: Image(systemName: \"star\"), name: \"Favourites\", tab: 1),\n    TabMenuItem(image: Image(systemName: \"bookmark\"), name: \"Categories\", tab: 2),\n    TabMenuItem(image: Image(systemName: \"books.vertical\"), name: \"About\", tab: 3)\n]\n\nTabMenuView(selection: $selection, items: items) { isSelected in\n    Group {\n        if isSelected {\n            Color.accentColor\n        } else {\n            Color(.secondaryLabel)\n        }\n    }\n} onReselect: {\n    NamedAction(\"Reselect\") {\n        print(\"TabMenu item \\(selection) reselected\")\n    }\n} onDoubleTap: {\n    NamedAction(\"Double Tap\") {\n        print(\"TabMenu item \\(selection) doubletapped\")\n    }\n}\n```\n\n## Widgets\n### AccessoryInlineImage\nAn image that will be scaled and have the rendering mode adjusted to work inside an `accessoryInline` widget. The image will scale to fit the frame and have the template rendering mode applied.\n\nUse inside a Label's icon property.\n\n```swift\nLabel {\n    Text(\"Label Text\")\n} icon: {\n    AccessoryInlineImage(\"myImage\")\n}\n```\n\n### WidgetSize\nAn enum similar to WidgetFamily but returns widget frame sizes by device and doesn't require `WidgetKit` so it can be used inside your main iOS or macOS app.\n\n#### `sizeForiPhone(screenSize:)\nReturns the size of the widget based on the screen size provided.\n\n#### `sizeForiPad(screenSize:, target:)\nReturns either the design canvas or the home screen size (depending on the supplied target) of the widget based on the screen size provided. On iPads widget content is put on the design canvas then scaled to fit the home screen size. (The `WidgetDemoFrame` will do this scaling for you)\n\n#### `supportedSizesForCurrentDevice` (iOS Only)\nReturns an array of supported widget sizes based on device type and iOS version.\n\n#### `sizeForCurrentDevice` (iOS Only)\nReturns the size of the widget based on the current device.\n\nAll widget size information was sourced from:\n[Apple - Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/widgets#Specifications)\n\n### WidgetDemoFrame\nCreates widget frames sized for a supplied screen size or the current device (iOS only). Used for showing example widgets from inside the app.\n\nCorner radius size defaults to 20 and may not be the same as the actual widget corner radius.\n\nFor iPad, widget views use a design size and are scaled to a smaller Home Screen size using `ScaledView`. This demo frame uses the same scaling to properly preview the widget. All sizes will work on all devices and all versions of iOS (even extraLarge on iPhone with iOS 14.0).\n\n```swift\nWidgetDemoFrame(.medium, cornerRadius: 20) { size, cornerRadius in\n    Text(\"Demo Widget\")\n}\n```\n\n## Additional Tools\n### Proportionable\nA protocol that adds helpful parameters like `aspectFormat`, `aspectRatio`, `minDimension`, `maxDimension`, `init(width:,aspectRatio:)`, `init(height:,aspectRatio:)`, `square(:)`, `scaledToFit(:)`, and `scaledToFill(:)`.\n\nUsed on types that have `width` and `height` properties like `CGSize`.\n\nHow to add conformance in your app:\n```swift\nextension CGSize: Proportionable { }\n```\n\n### frame(size:,alignment:)\nAlternative to the `frame(width:,height:,alignment:)` View modifier that takes a `CGSize` parameter instead.\n\n----\n# Features for iOS 14+15\n\n## FULayout\nIf you like the SwiftUI `Layout` protocol but you need to target an older OS that doesn't support it then the `FULayout` protocol might be your answer!\n\nAn `FULayout` will work in the same way as a SwiftUI `Layout`. The main difference is it will require a `maxWidth` or `maxHeight` parameter when initializing in order to know the available space. This can be provided by `GeometryReader` or with [`WidthReader`](#widthreader) or [`HeightReader`](#heightreader) from this package.\n\n### ViewBuilder\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nAn `FULayout` uses `callAsFunction()` with a view builder so you can use it just like a SwiftUI `Layout`.\n\n```swift\nVFlow(maxWidth: 200) {\n    Text(\"Hello\")\n    Text(\"World\")\n}\n```\n\n*Caution: This method uses Apple's private protocol `_VariadicView` under the hood. There is a small risk Apple could change the implementation so if this concerns you, use method 2 below.*\n\n### `.forEach()`\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nThis method works in a very similar way to `ForEach()`.\n\n```swift\nMyFULayout().forEach([\"Hello\", \"World\"], id: \\.self) { item in\n        Text(item.value)\n    }\n}\n```\n\n## FULayouts\n### HFlow\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nThe [`FULayout`](#fulayout) equivalent of [`HFlowLayout`](#hflowlayout).\n\nA FrameUp `FULayout` that arranges views in horizontal rows flowing from one to the next with adjustable horizontal and vertical spacing and support for horiztonal and vertical alignment including a justified alignment that will space elements in completed rows evenly.\n\nEach row height will be determined by the tallest view in that row.\n\n```swift\nWidthReader { width in\n    HFlow(maxWidth: width) {\n        ForEach([\"Hello\", \"World\", \"More Text\"], id: \\.self) { item in\n            Text(item.value)\n        }\n    }\n}\n```\n\n### VFlow\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nThe [`FULayout`](#fulayout) equivalent of [`VFlowLayout`](#vflowlayout).\n\nA FrameUp `FULayout` that arranges views in vertical columns flowing from one to the next with adjustable horizontal and vertical spacing and support for horiztonal and vertical alignment including a justified alignment that will space elements in completed columns evenly.\n\nEach column width will be determined by the widest element.\n\n```swift\nWidthReader { width in\n    VFlow(maxWidth: width) {\n        ForEach([\"Hello\", \"World\", \"More Text\"], id: \\.self) { item in\n            Text(item.value)\n        }\n    }\n}\n```\n \n### HMasonry\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nThe [`FULayout`](#fulayout) equivalent of [`HMasonryLayout`](#hmasonrylayout).\n\nA FrameUp `FULayout` that arranges views into a set number of rows by adding each view to the shortest row.\n\n```swift\nHeightReader { height in\n    HMasonry(columns: 3, maxHeight: height) {\n        ForEach([\"Hello\", \"World\", \"More Text\"], id: \\.self) { item in\n            Text(item.value)\n                .frame(maxHeight: .infinity, alignment: .center)\n        }\n    }\n}\n```\n\n### VMasonry\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nThe [`FULayout`](#fulayout) equivalent of [`VMasonryLayout`](#vmasonrylayout).\n\nA FrameUp `FULayout` that arranges views into a set number of rows by adding each view to the shortest row.\n\n```swift\nWidthReader { width in\n    VMasonry(columns: 3, maxWidth: width) {\n        ForEach([\"Hello\", \"World\", \"More Text\"], id: \\.self) { item in\n            Text(item.value)\n                .frame(maxWidth: .infinity, alignment: .center)\n        }\n    }\n}\n```\n\n### FULayoutThatFits\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nThe [`FULayout`](#fulayout) equivalent of [`LayoutThatFits`](#layoutthatfits).\n\nAn `FULayout` that picks the first provided layout that will fit the content in the provided maxWidth, maxHeight, or both. This is most helpful when switching between `HStackFULayout` and `VStackFULayout` as the content only needs to be provided once and will even animate when the stack changes.\n\n```swift\nFULayoutThatFits(\n    maxWidth: maxWidth,\n    layouts: [\n        HStackFULayout(maxHeight: 1000),\n        VStackFULayout(maxWidth: maxWidth)\n    ]\n) {\n    Color.green.frame(width: 50, height: 50)\n    Color.yellow.frame(width: 50, height: 200)\n    Color.blue.frame(width: 50, height: 100)\n}\n```\n\n### FUViewThatFits\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nThe [`FULayout`](#fulayout) equivalent of SwiftUI `ViewThatFits`.\n\nAn `FULayout` that presents the first view that fits the provided maxWidth, maxHeight, or both depending on which parameters are used.\n\nAs this view cannot measure the available space the maxWidth and/or maxHeight parameters need to be passed in using a `GeometryReader`, `WidthReader`, or `HeightReader`.\n\n```swift\nWidthReader { width in\n    FUViewThatFits(maxWidth: width) {\n        Group {\n            Text(\"This layout will pick the first view that fits the available width.\")\n            Text(\"Maybe this?\")\n            Text(\"OK!\")\n        }\n        .fixedSize(horizontal: true, vertical: false)\n    }\n}\n```\n\n(`.fixedSize` needs to be used in this example or the first view will automatically fit by truncating the text)\n\n### FULayout Stacks\nAlternative stack layouts that can be wrapped in [`AnyFULayout`](#anyfulayout) and then toggled between with animation. Useful when you want to toggle between VStack and HStack based on available space.\n\n#### HStackFULayout\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nSimilar to `HStack` but `Spacer()` cannot be used and content will always use a fixed size on the horizontal axis.\n\n#### VStackFULayout\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nSimilar to `VStack` but `Spacer()` cannot be used and content will always use a fixed size on the vertical axis.\n\n#### ZStackFULayout\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nSimilar to `ZStack` but content will always use a fixed size on both the vertical and horizontal axes.\n\n### AnyFULayout\n*\\*Deprecated iOS 16, macOS 13, watchOS 7, tvOS 14, visionOS 1*\nThe [`FULayout`](#fulayout) equivalent of SwiftUI `AnyLayout`.\n\nA type-erased FrameUp layout can be used to wrap multiple layouts and switch between them with animation.\n\n```swift\nstruct AnyFULayoutSimple: View {\n    let isVStack: Bool\n    let maxSize: CGSize\n    \n    var layout: any FULayout {\n        isVStack ? VStackFULayout(maxWidth: maxSize.width) : HStackFULayout(maxHeight: maxSize.height)\n    }\n    \n    var body: some View {\n        AnyFULayout(layout) {\n            Text(\"First\")\n            Text(\"Second\")\n            Text(\"Third\")\n        }\n        .animation(.spring(), value: isVStack)\n    }\n}\n```\n\n### Custom FULayout\n\nThe FrameUp [`FULayout`](#fulayout) protocol requires you to define which axes are fixed, the maximum item size, and a function that takes view sizes and outputs view offsets.\n\nBelow is an example layout that arranges views on left and right sides of a central line.\n\n```swift\nstruct CustomFULayout: FULayout {\n    /// Add parameters here to adjust layout\n    \n    /// Define these required parameters\n    var fixedSize: Axis.Set = .horizontal\n    var maxItemWidth: CGFloat? { maxWidth }\n    var maxItemHeight: CGFloat? = nil\n    \n    func contentOffsets(sizes: [Int : CGSize]) -\u003e [Int : CGPoint] {\n        /// Write code that uses the dictionary of sizes and your parameters to output a dictionary of offsets from the top left corner.\n    }\n}\n```\n\n### LayoutFromFULayout\n\nIf you've created an [`FULayout`](#fulayout) you can use it to easily create a SwiftUI `Layout`.\n\n```swift\nstruct CustomLayout: LayoutFromtFULayout {\n    /// Add parameters here to adjust layout\n    \n    /// Add this function that will create the associated FULayout\n    func fuLayout(maxSize: CGSize) -\u003e CustomFULayout {\n        CustomFULayout(\n            /// Pass parameters through to FULayout using maxSize to help define the maximum item size.\n        )\n    }\n}\n```\n\n## TagView\nAn older and much simpler version of [`HFlow`](#hflow) not based on `FULayout`.\n\n### TagView\nA view that creates views based on an array of elements from left to right, adding rows when needed. Each row height will be determined by the tallest element.\n\n*Warning: Does not work in ScrollView.*\n\n```swift\nTagView(elements: [\"One\", \"Two\", \"Three\"]) { element in\n    Text(element)\n}\n```\n\n### TagViewForScrollView\nA view that creates views based on an array of elements from left to right, adding rows when needed. Each row height will be determined by the tallest element.\n\nA maximum width must be provided but `WidthReader` can be used to get the value.\n\n```swift\nWidthReader { width in\n    TagView(maxWidth: width, elements: [\"One\", \"Two\", \"Three\"]) { element in\n        Text(element)\n    }\n}\n```\n\n## Widget bugfixes\n\n### WidgetRelativeShape\n*\\*iOS only*\n\nA re-scaled version of `ContainerRelativeShape` used to fix a bug with the corner radius on iPads running iOS 15 and earlier. It acts exactly the same as ContainerRelativeShape for iOS 16 and up.\n\nThis example has a blue background with a 1 point inset. On an iPad running iOS 15 or earlier, the red background will show on the corners as the corner radius does not match.\n\n```swift\nText(\"Example widget\")\n    .background(.blue)\n    .clipShape(WidgetRelativeShape(.systemSmall))\n    .background(\n        ContainerRelativeShape()\n            .fill(.red)\n    )\n    .padding(1)\n```\n\n\n\n\n\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanlintott%2Fframeup","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryanlintott%2Fframeup","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanlintott%2Fframeup/lists"}