├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Example ├── FrameUpExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── FrameUpExample.xcscheme │ │ └── WidgetFrameWidgetExtension.xcscheme ├── FrameUpExample │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-MacOS-128x128@1x.png │ │ │ ├── Icon-MacOS-128x128@2x.png │ │ │ ├── Icon-MacOS-16x16@1x.png │ │ │ ├── Icon-MacOS-16x16@2x.png │ │ │ ├── Icon-MacOS-256x256@1x.png │ │ │ ├── Icon-MacOS-256x256@2x.png │ │ │ ├── Icon-MacOS-32x32@1x.png │ │ │ ├── Icon-MacOS-32x32@2x.png │ │ │ ├── Icon-MacOS-512x512@1x.png │ │ │ ├── Icon-MacOS-512x512@2x.png │ │ │ └── app-icon-1024@1x~ios-marketing.png │ │ ├── AppIcon.brandassets │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── FrameUp-icon-tvOS-back.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── FrameUp-icon-tvOS-front.png │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── FrameUp-icon-tvOS-middle.png │ │ │ │ │ └── Contents.json │ │ │ ├── App Icon.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── FrameUp-icon-tvOS-back400.png │ │ │ │ │ │ └── FrameUp-icon-tvOS-back800.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── FrameUp-icon-tvOS-front400.png │ │ │ │ │ │ └── FrameUp-icon-tvOS-front800.png │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── FrameUp-icon-tvOS-middle400.png │ │ │ │ │ └── FrameUp-icon-tvOS-middle800.png │ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── FrameUp-topShelfWide-tvOS.png │ │ │ │ └── FrameUp-topShelfWide-tvOS1440.png │ │ │ └── Top Shelf Image.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── FrameUp-topShelf-tvOS.png │ │ │ │ └── FrameUp-topShelf-tvOS1440.png │ │ ├── AppIcon.solidimagestack │ │ │ ├── Back.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── FrameUp-icon-visionOS-back.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── FrameUp-icon-visionOS-front.png │ │ │ │ └── Contents.json │ │ │ └── Middle.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── FrameUp-icon-visionOS-middle.png │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── FrameUp-logo.imageset │ │ │ ├── Contents.json │ │ │ └── FrameUp-logo.png │ ├── AutoRotatingViewExamples │ │ ├── AutoRotatingViewExample.swift │ │ └── AutoRotatingViewExamples.swift │ ├── ContentView.swift │ ├── Experiments │ │ ├── DoubleScrollTabView.swift │ │ ├── ExperimentViews.swift │ │ ├── HFlowSmartScrollViewExample.swift │ │ └── SizeMatchingViewExample.swift │ ├── FULayoutExamples │ │ ├── AnyFULayoutExample.swift │ │ ├── AnyFULayoutHorizontalExample.swift │ │ ├── AnyFULayoutSimpleExample.swift │ │ ├── CustomFULayoutExample.swift │ │ ├── FULayoutExamples.swift │ │ ├── FULayoutThatFitsExample.swift │ │ ├── FUViewThatFitsExample.swift │ │ ├── HFlowBoxExample.swift │ │ ├── HFlowExample.swift │ │ ├── HMasonryAspectRatioExample.swift │ │ ├── HMasonryExample.swift │ │ ├── HStackFULayoutExample.swift │ │ ├── VFlowExample.swift │ │ ├── VMasonryAspectRatioExample.swift │ │ ├── VMasonryExample.swift │ │ ├── VStackFULayoutExample.swift │ │ └── ZStackFULayoutExample.swift │ ├── FlippingViewExamples │ │ ├── FlippingViewExample.swift │ │ ├── FlippingViewExamples.swift │ │ ├── PerspectiveFlippingViewExample.swift │ │ ├── TwoSided3DViewExample.swift │ │ └── TwoSidedViewExample.swift │ ├── FrameAdjustmentExamples │ │ ├── EqualHeightExample.swift │ │ ├── EqualWidthExample.swift │ │ ├── FrameAdjustmentExamples.swift │ │ ├── HeightReaderExample.swift │ │ ├── KeyboardHeightExample.swift │ │ ├── OnSizeChangeExample.swift │ │ ├── OverlappingImageHorizontalExample.swift │ │ ├── OverlappingImageVerticalExample.swift │ │ ├── RelativePaddingExample.swift │ │ ├── ScaledToFrameExample.swift │ │ └── WidthReaderExample.swift │ ├── FrameUpExampleApp.swift │ ├── Item.swift │ ├── LayoutExamples │ │ ├── CustomLayoutExample.swift │ │ ├── HFlowBoxLayoutExample.swift │ │ ├── HFlowLayoutExample.swift │ │ ├── HMasonryLayoutExample.swift │ │ ├── LayoutExamples.swift │ │ ├── LayoutThatFitsExample.swift │ │ ├── VFlowLayoutExample.swift │ │ └── VMasonryLayoutExample.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── SmartScrollViewExamples │ │ ├── SmartScrollViewExample.swift │ │ ├── SmartScrollViewExamples.swift │ │ └── SmartScrollViewSimpleExample.swift │ ├── TabMenuExamples │ │ ├── TabMenuExample.swift │ │ ├── TabMenuExampleView.swift │ │ └── TabMenuExamples.swift │ ├── TagViewExamples │ │ ├── TagViewExample.swift │ │ ├── TagViewExamples.swift │ │ └── TagViewForScrollViewExample.swift │ ├── TextExamples │ │ ├── HairSpaceJustifiedTextExample.swift │ │ ├── TextExamples.swift │ │ └── UnclippedTextExample.swift │ ├── UnavailableView.swift │ ├── WidgetExamples │ │ ├── WidgetDemoFrameExample.swift │ │ ├── WidgetExamples.swift │ │ ├── WidgetRelativeShapeDemo.swift │ │ ├── WidgetRelativeShapeExample.swift │ │ └── WidgetSizeExample.swift │ └── _Extensions │ │ └── View+IfAvailable.swift ├── FrameUpWatchExample Watch App │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ └── FrameUp-icon-control1024.png │ │ └── Contents.json │ ├── ContentView.swift │ ├── FrameUpWatchExampleApp.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Shared │ ├── CrossPlatform │ │ ├── CrossPlatformSlider.swift │ │ └── CrossPlatformStepper.swift │ └── SharedAssets.xcassets │ │ ├── Contents.json │ │ ├── FrameUp-icon-control.imageset │ │ ├── Contents.json │ │ └── FrameUp-icon-control.png │ │ └── FrameUp-logo-alpha.imageset │ │ ├── Contents.json │ │ ├── FrameUp-logo-alpha@1x.png │ │ ├── FrameUp-logo-alpha@2x.png │ │ └── FrameUp-logo-alpha@3x.png └── WidgetFrameWidget │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── WidgetBackground.colorset │ │ └── Contents.json │ ├── DateTimelineProvider.swift │ ├── FrameUpWidgetBundle.swift │ ├── Info.plist │ ├── InlineImageWidget.swift │ ├── OpenAppAppIntent.swift │ ├── Views │ ├── InlineImageExample.swift │ ├── JustifiedTextExample.swift │ └── WidgetRelativeShapeExample.swift │ └── WidgetFrameWidget.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── FrameUp │ ├── AutoRotatingView │ ├── AutoRotatingView.swift │ ├── FUInterfaceOrientation.swift │ ├── InfoDictionary.swift │ ├── RotationMatchingOrientationViewModifier.swift │ └── UIDeviceOrientation-extension.swift │ ├── FULayout │ ├── AnyFULayout.swift │ ├── FUAlignment.swift │ ├── FULayout+callAsFunction.swift │ ├── FULayout+forEach.swift │ ├── FULayout.swift │ ├── FULayoutColumn.swift │ ├── FULayoutRootAndChildView.swift │ ├── FULayoutRow.swift │ ├── FULayoutSizeKey.swift │ └── FULayouts │ │ ├── FULayoutThatFits.swift │ │ ├── FUViewThatFits.swift │ │ ├── HFlow.swift │ │ ├── HMasonry.swift │ │ ├── HStackFULayout.swift │ │ ├── VFlow.swift │ │ ├── VMasonry.swift │ │ ├── VStackFULayout.swift │ │ └── ZStackFULayout.swift │ ├── FrameAdjustment │ ├── EqualSize │ │ ├── EqualHeight.swift │ │ └── EqualWidth.swift │ ├── OverlappingImage │ │ ├── OverlappingImage+UIImage.swift │ │ └── OverlappingImage.swift │ ├── Proportionable │ │ ├── AspectFormat.swift │ │ └── Proportionable.swift │ ├── Readers │ │ ├── HeightReader.swift │ │ ├── KeyboardHeightEnvironmentValue.swift │ │ ├── OnSizeChange.swift │ │ └── WidthReader.swift │ ├── RelativePaddingViewModifier.swift │ └── ScaledView.swift │ ├── Layout │ ├── LayoutFromFULayout.swift │ └── Layouts │ │ ├── FittedHStack.swift │ │ ├── FittedVStack.swift │ │ ├── HFlowLayout.swift │ │ ├── HMasonryLayout.swift │ │ ├── LayoutThatFits.swift │ │ ├── VFlowLayout.swift │ │ └── VMasonryLayout.swift │ ├── PrivacyInfo.xcprivacy │ ├── SmartScrollView │ └── SmartScrollView.swift │ ├── TabMenu │ ├── NamedAction.swift │ ├── TabMenu.swift │ └── TabMenuItem.swift │ ├── TagView │ ├── TagView.swift │ └── TagViewForScrollView.swift │ ├── Text │ ├── HairSpaceJustifiedText.swift │ └── UnclippedTextRenderer.swift │ ├── TwoSidedView │ ├── BackfaceCull.swift │ ├── FlippingView.swift │ ├── PerspectiveFlippingView.swift │ ├── TwoSided3DView.swift │ └── TwoSidedView.swift │ ├── Widget │ ├── AccessoryInlineImage.swift │ ├── WidgetDemoFrame.swift │ ├── WidgetFamily+extensions.swift │ ├── WidgetRelativeShape.swift │ ├── WidgetSize+CurrentDevice.swift │ ├── WidgetSize+WidgetKit.swift │ └── WidgetSize.swift │ ├── _Deprecated │ └── Flow │ │ ├── HFlowLegacy.swift │ │ ├── LegacyFlowContentSizeKey.swift │ │ ├── VFlowLegacy.swift │ │ └── VGridMasonry.swift │ ├── _Extensions-Internal │ ├── CG+equals.swift │ ├── CGSize+extensions.swift │ ├── Dictionary+extensions.swift │ ├── View+IfAvailable.swift │ └── View+onPreferenceChange.swift │ └── _Extensions-Public │ ├── Axis.Set+Hashable.swift │ ├── Dictionary+publicExtensions.swift │ ├── UIImage+publicExtensions.swift │ └── View+publicExtensions.swift └── Tests └── FrameUpTests └── FrameUpTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | ### Swift ### 14 | # Xcode 15 | # 16 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 17 | 18 | ## User settings 19 | xcuserdata/ 20 | 21 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 22 | *.xcscmblueprint 23 | *.xccheckout 24 | 25 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 26 | build/ 27 | DerivedData/ 28 | *.moved-aside 29 | *.pbxuser 30 | !default.pbxuser 31 | *.mode1v3 32 | !default.mode1v3 33 | *.mode2v3 34 | !default.mode2v3 35 | *.perspectivev3 36 | !default.perspectivev3 37 | 38 | ## Obj-C/Swift specific 39 | *.hmap 40 | 41 | ## App packaging 42 | *.ipa 43 | *.dSYM.zip 44 | *.dSYM 45 | 46 | ## Playgrounds 47 | timeline.xctimeline 48 | playground.xcworkspace 49 | 50 | # Swift Package Manager 51 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 52 | Packages/ 53 | Package.pins 54 | Package.resolved 55 | # *.xcodeproj 56 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 57 | # hence it is not needed unless you have added a package configuration file to your project 58 | .swiftpm 59 | 60 | .build/ 61 | 62 | # CocoaPods 63 | # We recommend against adding the Pods directory to your .gitignore. However 64 | # you should judge for yourself, the pros and cons are mentioned at: 65 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 66 | # Pods/ 67 | # Add this line if you want to avoid checking in source code from the Xcode workspace 68 | # *.xcworkspace 69 | 70 | # Carthage 71 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 72 | # Carthage/Checkouts 73 | 74 | Carthage/Build/ 75 | 76 | # Accio dependency management 77 | Dependencies/ 78 | .accio/ 79 | 80 | # fastlane 81 | # It is recommended to not store the screenshots in the git repo. 82 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 83 | # For more information about the recommended setup visit: 84 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 85 | 86 | fastlane/report.xml 87 | fastlane/Preview.html 88 | fastlane/screenshots/**/*.png 89 | fastlane/test_output 90 | 91 | # Code Injection 92 | # After new code Injection tools there's a generated folder /iOSInjectionProject 93 | # https://github.com/johnno1962/injectionforxcode 94 | 95 | iOSInjectionProject/ 96 | 97 | # End of https://www.toptal.com/developers/gitignore/api/swift,macos -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FrameUpExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/FrameUpExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024@1x~ios-marketing.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "filename" : "Icon-MacOS-16x16@1x.png", 11 | "idiom" : "mac", 12 | "scale" : "1x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-MacOS-16x16@2x.png", 17 | "idiom" : "mac", 18 | "scale" : "2x", 19 | "size" : "16x16" 20 | }, 21 | { 22 | "filename" : "Icon-MacOS-32x32@1x.png", 23 | "idiom" : "mac", 24 | "scale" : "1x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-MacOS-32x32@2x.png", 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "32x32" 32 | }, 33 | { 34 | "filename" : "Icon-MacOS-128x128@1x.png", 35 | "idiom" : "mac", 36 | "scale" : "1x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-MacOS-128x128@2x.png", 41 | "idiom" : "mac", 42 | "scale" : "2x", 43 | "size" : "128x128" 44 | }, 45 | { 46 | "filename" : "Icon-MacOS-256x256@1x.png", 47 | "idiom" : "mac", 48 | "scale" : "1x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-MacOS-256x256@2x.png", 53 | "idiom" : "mac", 54 | "scale" : "2x", 55 | "size" : "256x256" 56 | }, 57 | { 58 | "filename" : "Icon-MacOS-512x512@1x.png", 59 | "idiom" : "mac", 60 | "scale" : "1x", 61 | "size" : "512x512" 62 | }, 63 | { 64 | "filename" : "Icon-MacOS-512x512@2x.png", 65 | "idiom" : "mac", 66 | "scale" : "2x", 67 | "size" : "512x512" 68 | } 69 | ], 70 | "info" : { 71 | "author" : "xcode", 72 | "version" : 1 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/app-icon-1024@1x~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.appiconset/app-icon-1024@1x~ios-marketing.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-tvOS-back.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-back.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-tvOS-front.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-front.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-tvOS-middle.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-middle.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-tvOS-back400.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "FrameUp-icon-tvOS-back800.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-back400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-back400.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-back800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-back800.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-tvOS-front400.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "FrameUp-icon-tvOS-front800.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-front400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-front400.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-front800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-front800.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-tvOS-middle400.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "FrameUp-icon-tvOS-middle800.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-middle400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-middle400.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-middle800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/FrameUp-icon-tvOS-middle800.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-topShelfWide-tvOS.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "FrameUp-topShelfWide-tvOS1440.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/FrameUp-topShelfWide-tvOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/FrameUp-topShelfWide-tvOS.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/FrameUp-topShelfWide-tvOS1440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/FrameUp-topShelfWide-tvOS1440.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-topShelf-tvOS.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "FrameUp-topShelf-tvOS1440.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/FrameUp-topShelf-tvOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/FrameUp-topShelf-tvOS.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/FrameUp-topShelf-tvOS1440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/FrameUp-topShelf-tvOS1440.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-visionOS-back.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/FrameUp-icon-visionOS-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/FrameUp-icon-visionOS-back.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.solidimagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.solidimagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.solidimagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-visionOS-front.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/FrameUp-icon-visionOS-front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/FrameUp-icon-visionOS-front.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-visionOS-middle.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/FrameUp-icon-visionOS-middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/FrameUp-icon-visionOS-middle.png -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/FrameUp-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Assets.xcassets/FrameUp-logo.imageset/FrameUp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpExample/Assets.xcassets/FrameUp-logo.imageset/FrameUp-logo.png -------------------------------------------------------------------------------- /Example/FrameUpExample/AutoRotatingViewExamples/AutoRotatingViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoRotatingViewExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-04-05. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | #if os(iOS) 12 | struct AutoRotatingViewExample: View { 13 | @State private var isAnimated: Bool = true 14 | @State private var portrait: Bool = true 15 | @State private var landscapeLeft: Bool = true 16 | @State private var landscapeRight: Bool = true 17 | @State private var portraitUpsideDown: Bool = true 18 | 19 | var allowedOrientations: [FUInterfaceOrientation] { 20 | zip( 21 | [ 22 | portrait, 23 | landscapeLeft, 24 | landscapeRight, 25 | portraitUpsideDown 26 | ], 27 | [ 28 | .portrait, 29 | .landscapeLeft, 30 | .landscapeRight, 31 | .portraitUpsideDown 32 | ] 33 | ) 34 | .compactMap { $0 ? $1 : nil } 35 | } 36 | 37 | func face(text: String) -> some View { 38 | VStack { 39 | Image(systemName: "face.smiling") 40 | .resizable() 41 | .scaledToFit() 42 | .frame(height: 40) 43 | Button { 44 | print("clicked") 45 | } label: { 46 | Text(text) 47 | } 48 | } 49 | } 50 | 51 | var body: some View { 52 | VStack { 53 | AutoRotatingView(allowedOrientations, animation: isAnimated ? .default : nil) { 54 | VStack { 55 | Image("FrameUp-logo") 56 | .resizable() 57 | .scaledToFit() 58 | .frame(height: 40) 59 | 60 | Text("This view can auto-rotate to orientations the app does not support.") 61 | .font(.caption) 62 | } 63 | .foregroundColor(.white) 64 | .padding() 65 | .background(Color.blue) 66 | .cornerRadius(20) 67 | } 68 | 69 | VStack(alignment: .leading) { 70 | Toggle("Animation", isOn: $isAnimated) 71 | Section(header: Text("Allowed Orientations:").font(.headline)) { 72 | Toggle("Portrait", isOn: $portrait) 73 | Toggle("LandscapeLeft", isOn: $landscapeLeft) 74 | Toggle("LandscapeRight", isOn: $landscapeRight) 75 | Toggle("PortraitUpsideDown", isOn: $portraitUpsideDown) 76 | } 77 | } 78 | .padding() 79 | } 80 | .navigationTitle("AutoRotatingView") 81 | } 82 | } 83 | 84 | struct AutoRotatingViewExample_Previews: PreviewProvider { 85 | static var previews: some View { 86 | AutoRotatingViewExample() 87 | } 88 | } 89 | #endif 90 | -------------------------------------------------------------------------------- /Example/FrameUpExample/AutoRotatingViewExamples/AutoRotatingViewExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoRotatingViewExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AutoRotatingViewExamples: View { 11 | var body: some View { 12 | Section { 13 | #if os(iOS) 14 | NavigationLink(destination: AutoRotatingViewExample()) { 15 | Label("AutoRotatingView", systemImage: "arrow.turn.up.forward.iphone") 16 | } 17 | #else 18 | UnavailableView() 19 | #endif 20 | } header: { 21 | Text("AutoRotatingView") 22 | } 23 | } 24 | } 25 | 26 | struct AutoRotatingViewExamples_Previews: PreviewProvider { 27 | static var previews: some View { 28 | NavigationView { 29 | List { 30 | AutoRotatingViewExamples() 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Example/FrameUpExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-14. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | var logo: some View { 13 | Image("FrameUp-logo") 14 | .resizable() 15 | .scaledToFit() 16 | .frame(maxWidth: 400) 17 | .padding() 18 | } 19 | 20 | @ViewBuilder 21 | var examples: some View { 22 | LayoutExamples() 23 | 24 | AutoRotatingViewExamples() 25 | 26 | FrameAdjustmentExamples() 27 | 28 | TextExamples() 29 | 30 | SmartScrollViewExamples() 31 | 32 | FlippingViewExamples() 33 | 34 | TabMenuExamples() 35 | 36 | WidgetExamples() 37 | 38 | FULayoutExamples() 39 | 40 | TagViewExamples() 41 | 42 | /// These likely won't work 43 | ExperimentViews() 44 | } 45 | 46 | var body: some View { 47 | #if os(iOS) || os(tvOS) 48 | NavigationView { 49 | VStack { 50 | logo 51 | 52 | List { 53 | examples 54 | } 55 | } 56 | .navigationTitle("FrameUp") 57 | .navigationBarHidden(true) 58 | } 59 | .keyboardHeightEnvironmentValue() 60 | #else 61 | if #available(macOS 13, *) { 62 | NavigationSplitView { 63 | List { 64 | logo 65 | 66 | examples 67 | } 68 | .navigationTitle("FrameUp") 69 | } detail: { 70 | Text("Select an example.") 71 | } 72 | } else { 73 | NavigationView { 74 | List { 75 | logo 76 | 77 | examples 78 | } 79 | .navigationTitle("FrameUp") 80 | 81 | Text("Select an example.") 82 | } 83 | } 84 | #endif 85 | } 86 | } 87 | 88 | struct ContentView_Previews: PreviewProvider { 89 | static var previews: some View { 90 | ContentView() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Experiments/ExperimentViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExperimentViews.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ExperimentViews: View { 11 | var body: some View { 12 | #if os(iOS) 13 | Section { 14 | NavigationLink(destination: DoubleScrollTabViewExample()) { 15 | Label("DoubleScrollTabView", systemImage: "scroll") 16 | } 17 | 18 | NavigationLink(destination: HFlowSmartScrollViewExample()) { 19 | Label("HFlowSmartScrollView", systemImage: "arrow.forward.square") 20 | } 21 | } header: { 22 | Text("Experiments") 23 | } footer: { 24 | Text("These are buggy and may crash the app.") 25 | } 26 | #else 27 | EmptyView() 28 | #endif 29 | } 30 | } 31 | 32 | struct ExperimentViews_Previews: PreviewProvider { 33 | static var previews: some View { 34 | List { 35 | ExperimentViews() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Experiments/SizeMatchingViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SizeMatchingViewExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-07-10. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct SizeMatchingViewExample: View { 12 | @State private var size: CGSize? = nil 13 | 14 | var body: some View { 15 | ZStack { 16 | Color.blue.frame(width: 300, height: 300) 17 | 18 | Text("Size to match") 19 | .onSizeChange { 20 | size = $0 21 | } 22 | } 23 | .accessibilityElement(children: .combine) 24 | .overlay( 25 | Rectangle() 26 | .stroke(Color.red) 27 | .frame(maxWidth: size?.width, maxHeight: size?.height) 28 | ) 29 | } 30 | } 31 | 32 | #Preview { 33 | SizeMatchingViewExample() 34 | } 35 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FULayoutExamples/AnyFULayoutSimpleExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyFULayoutSimpleExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-09-14. 6 | // 7 | 8 | #if !os(visionOS) 9 | import FrameUp 10 | import SwiftUI 11 | 12 | struct AnyFULayoutSimple: View { 13 | let isVStack: Bool 14 | let maxSize: CGSize 15 | 16 | var layout: any FULayout { 17 | isVStack ? VStackFULayout(maxWidth: maxSize.width) : HStackFULayout(maxHeight: maxSize.height) 18 | } 19 | 20 | var body: some View { 21 | AnyFULayout(layout) { 22 | Text("First") 23 | Text("Second") 24 | Text("Third") 25 | } 26 | .foregroundColor(.white) 27 | .padding() 28 | .background(Color.blue) 29 | .animation(.spring(), value: isVStack) 30 | } 31 | } 32 | 33 | struct AnyFULayoutSimpleExample: View { 34 | @State private var isVStack: Bool = true 35 | 36 | var body: some View { 37 | VStack { 38 | GeometryReader { proxy in 39 | Color.clear.overlay( 40 | AnyFULayoutSimple(isVStack: isVStack, maxSize: proxy.size) 41 | ) 42 | } 43 | 44 | VStack { 45 | Text("AnyFULayout can animate between layouts keeping view ids.") 46 | 47 | Toggle("Use VStack", isOn: $isVStack) 48 | } 49 | .padding() 50 | } 51 | .navigationTitle("AnyFULayout Simple") 52 | } 53 | } 54 | 55 | struct AnyFULayoutSimpleExample_Previews: PreviewProvider { 56 | static var previews: some View { 57 | NavigationView { 58 | AnyFULayoutSimpleExample() 59 | } 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FULayoutExamples/CustomFULayoutExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomFULayoutExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-09-15. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct PingPong: FULayout { 12 | let maxWidth: CGFloat 13 | 14 | let fixedSize: Axis.Set = .horizontal 15 | var maxItemWidth: CGFloat? { maxWidth } 16 | let maxItemHeight: CGFloat? = nil 17 | 18 | init(maxWidth: CGFloat) { 19 | self.maxWidth = maxWidth 20 | } 21 | 22 | func contentOffsets(sizes: [Int : CGSize]) -> [Int : CGPoint] { 23 | var widthOffset = 0.0 24 | var heightOffset = 0.0 25 | var leftToRight = true 26 | var offsets = [Int : CGPoint]() 27 | for size in sizes.sortedByKey() { 28 | if leftToRight { 29 | if widthOffset + size.value.width > maxWidth { 30 | leftToRight = false 31 | widthOffset = maxWidth - size.value.width 32 | } 33 | } else { 34 | if widthOffset > size.value.width { 35 | widthOffset -= size.value.width 36 | } else { 37 | leftToRight = true 38 | widthOffset = .zero 39 | } 40 | } 41 | offsets.updateValue( 42 | CGPoint(x: widthOffset, y: heightOffset), 43 | forKey: size.key 44 | ) 45 | if leftToRight { 46 | widthOffset += size.value.width 47 | } 48 | 49 | heightOffset += size.value.height 50 | } 51 | 52 | return offsets 53 | } 54 | } 55 | 56 | #if !os(visionOS) 57 | struct CustomFULayoutExample: View { 58 | var body: some View { 59 | VStack { 60 | WidthReader { width in 61 | PingPong(maxWidth: width) { 62 | Group { 63 | Text("One") 64 | Text("Two") 65 | Text("Three") 66 | Text("Four") 67 | Text("Five") 68 | Text("Six") 69 | Text("Seven") 70 | Text("Eight") 71 | Text("Nine") 72 | Text("Ten") 73 | } 74 | .font(.title) 75 | .foregroundColor(.white) 76 | .padding(5) 77 | .background(Color.blue.cornerRadius(5)) 78 | .padding(2) 79 | } 80 | } 81 | 82 | Text("Create your own custom layouts like this one using the FULayout protocol.") 83 | .padding() 84 | } 85 | .navigationTitle("Custom FULayout") 86 | } 87 | } 88 | 89 | struct CustomFULayoutExample_Previews: PreviewProvider { 90 | static var previews: some View { 91 | NavigationView { 92 | CustomFULayoutExample() 93 | } 94 | } 95 | } 96 | #endif 97 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FULayoutExamples/FULayoutThatFitsExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FULayoutThatFitsExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-11-01. 6 | // 7 | 8 | #if !os(visionOS) 9 | import FrameUp 10 | import SwiftUI 11 | 12 | struct FULayoutThatFitsExample: View { 13 | @State private var maxWidth: CGFloat = 200 14 | 15 | var body: some View { 16 | VStack { 17 | Spacer() 18 | Text("Above") 19 | 20 | FULayoutThatFits(maxWidth: maxWidth, layouts: [HStackFULayout(maxHeight: 1000), VStackFULayout(maxWidth: maxWidth)]) { 21 | Color.green.frame(width: 50, height: 50) 22 | Color.yellow.frame(width: 50, height: 200) 23 | Color.blue.frame(width: 50, height: 100) 24 | } 25 | .animation(.default, value: maxWidth) 26 | .frame(width: maxWidth) 27 | .border(Color.red) 28 | 29 | Text("Below") 30 | 31 | Spacer() 32 | 33 | VStack { 34 | 35 | HStack { 36 | #if os(tvOS) 37 | Text("Max Width \(maxWidth)") 38 | Button("-") { maxWidth = max(50, maxWidth - 50) } 39 | Button("+") { maxWidth = min(350, maxWidth + 50) } 40 | #else 41 | Text("Max Width") 42 | Slider(value: $maxWidth, in: 50...350) 43 | .padding() 44 | #endif 45 | } 46 | } 47 | .padding() 48 | } 49 | .navigationTitle("FULayoutThatFits") 50 | } 51 | } 52 | 53 | struct FULayoutThatFitsExample_Previews: PreviewProvider { 54 | static var previews: some View { 55 | NavigationView { 56 | FULayoutThatFitsExample() 57 | } 58 | } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FULayoutExamples/FUViewThatFitsExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FUViewThatFitsExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-10-31. 6 | // 7 | 8 | #if !os(visionOS) 9 | import FrameUp 10 | import SwiftUI 11 | 12 | struct FUViewThatFitsExample: View { 13 | @State private var maxWidth: CGFloat = 200 14 | @State private var maxHeight: CGFloat = 200 15 | 16 | @State private var fitHoriztonal: Bool = true 17 | @State private var fitVertical: Bool = true 18 | 19 | var fuViewThatFits: FUViewThatFits { 20 | switch (fitVertical, fitHoriztonal) { 21 | case (true, true): 22 | return FUViewThatFits(maxWidth: maxWidth, maxHeight: maxHeight) 23 | case (true, false): 24 | return FUViewThatFits(maxHeight: maxHeight) 25 | case (false, true): 26 | return FUViewThatFits(maxWidth: maxWidth) 27 | case (false, false): 28 | return FUViewThatFits(maxWidth: .infinity, maxHeight: .infinity) 29 | } 30 | } 31 | 32 | var body: some View { 33 | VStack { 34 | Spacer() 35 | 36 | fuViewThatFits { 37 | Color.green.frame(width: 300, height: 300) 38 | Color.yellow.frame(width: 200, height: 200) 39 | Color.blue.frame(width: 100, height: 100) 40 | } 41 | .frame(width: maxWidth, height: maxHeight) 42 | .border(Color.red) 43 | 44 | Spacer() 45 | 46 | VStack { 47 | Toggle("Fit Horizontal", isOn: $fitHoriztonal) 48 | HStack { 49 | #if os(tvOS) 50 | Text("Max Width \(maxWidth)") 51 | Button("-") { maxWidth = max(50, maxWidth - 50) } 52 | Button("+") { maxWidth = min(350, maxWidth + 50) } 53 | #else 54 | Text("Max Width") 55 | Slider(value: $maxWidth, in: 50...350) 56 | .padding() 57 | #endif 58 | } 59 | 60 | Toggle("Fit Vertical", isOn: $fitVertical) 61 | HStack { 62 | #if os(tvOS) 63 | Text("Max Height \(maxHeight)") 64 | Button("-") { maxHeight = max(50, maxHeight - 50) } 65 | Button("+") { maxHeight = min(350, maxHeight + 50) } 66 | #else 67 | Text("Max Height") 68 | Slider(value: $maxHeight, in: 50...350) 69 | .padding() 70 | #endif 71 | } 72 | } 73 | .padding() 74 | } 75 | .navigationTitle("FUViewThatFits") 76 | } 77 | } 78 | 79 | struct FUViewThatFitsExample_Previews: PreviewProvider { 80 | static var previews: some View { 81 | NavigationView { 82 | FUViewThatFitsExample() 83 | } 84 | } 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FULayoutExamples/HFlowBoxExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HFlowBoxExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-10-18. 6 | // 7 | 8 | #if !os(visionOS) 9 | import FrameUp 10 | import SwiftUI 11 | 12 | struct HFlowBoxExample: View { 13 | @State private var boxes = [1,2,3,4,5] 14 | @State private var horizontalAlignment: FUHorizontalAlignment = .leading 15 | let verticalAlignment: FUVerticalAlignment = .top 16 | 17 | var alignment: FUAlignment { .init(horizontal: horizontalAlignment, vertical: verticalAlignment)} 18 | 19 | var body: some View { 20 | VStack { 21 | Color.clear.overlay( 22 | ScrollView { 23 | WidthReader { width in 24 | HFlow(alignment: alignment, maxWidth: width) { 25 | ForEach(boxes, id: \.self) { box in 26 | Color.red 27 | .frame(width: 80, height: 80) 28 | .padding() 29 | } 30 | } 31 | } 32 | } 33 | .animation(.default, value: boxes) 34 | .animation(.default, value: alignment) 35 | ) 36 | 37 | VStack { 38 | HStack { 39 | Button("Remove Box") { if !boxes.isEmpty { boxes.removeLast() } } 40 | .padding() 41 | Button("Add Box") { boxes.append((boxes.max() ?? 1) + 1) } 42 | .padding() 43 | } 44 | 45 | Picker("Horizontal Alignment", selection: $horizontalAlignment) { 46 | ForEach(FUHorizontalAlignment.allCases) { 47 | Text($0.rawValue) 48 | } 49 | } 50 | .pickerStyle(.segmented) 51 | } 52 | .padding() 53 | } 54 | .navigationTitle("HFlow Boxes") 55 | } 56 | } 57 | 58 | struct HFlowBoxExample_Previews: PreviewProvider { 59 | static var previews: some View { 60 | NavigationView { 61 | HFlowBoxExample() 62 | } 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FlippingViewExamples/FlippingViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlippingViewExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-09-15. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct FlippingViewExample: View { 12 | @State private var flips: Int = 0 13 | var isFaceUp: Bool { flips.isMultiple(of: 2) } 14 | @State private var axis: Axis = .horizontal 15 | 16 | var body: some View { 17 | VStack { 18 | FlippingView( 19 | axis, 20 | flips: $flips 21 | ) { 22 | ZStack { 23 | Color.blue 24 | Text("Front") 25 | } 26 | .cornerRadius(20) 27 | } back: { 28 | ZStack { 29 | Color.red 30 | Text("Back") 31 | } 32 | .cornerRadius(20) 33 | } 34 | #if os(visionOS) 35 | .frame(maxWidth: 200, maxHeight: 200) 36 | .frame(maxDepth: 200, alignment: .center) 37 | #endif 38 | .font(.largeTitle) 39 | .padding(40) 40 | 41 | VStack { 42 | HStack { 43 | Text("Flips: \(flips)") 44 | Spacer() 45 | Text("Face \(isFaceUp ? "Up" : "Down")") 46 | } 47 | 48 | HStack { 49 | Text("Axis") 50 | Picker("Axis", selection: $axis) { 51 | ForEach(Axis.allCases, id: \.self) { axis in 52 | Text("\(axis.description)") 53 | } 54 | } 55 | .pickerStyle(.segmented) 56 | } 57 | 58 | HStack { 59 | Text("Programmatic flip") 60 | Button("-1") { flips -= 1 } 61 | Button("+1") { flips += 1 } 62 | } 63 | } 64 | .padding() 65 | } 66 | } 67 | } 68 | 69 | #Preview { 70 | FlippingViewExample() 71 | } 72 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FlippingViewExamples/FlippingViewExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlippingViewExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FlippingViewExamples: View { 11 | var body: some View { 12 | Section { 13 | #if os(visionOS) 14 | NavigationLink(destination: TwoSided3DViewExample()) { 15 | Label("rotation3DEffect(back:)", systemImage: "arrow.uturn.right.square") 16 | } 17 | 18 | NavigationLink(destination: TwoSidedViewExample()) { 19 | Label("perspectiveRotationEffect(back:)", systemImage: "arrow.uturn.right.square") 20 | } 21 | #else 22 | NavigationLink(destination: TwoSidedViewExample()) { 23 | Label("rotation3DEffect(back:)", systemImage: "arrow.uturn.right.square") 24 | } 25 | #endif 26 | 27 | NavigationLink(destination: FlippingViewExample()) { 28 | Label("FlippingView", systemImage: "arrow.uturn.right.square") 29 | } 30 | 31 | #if os(visionOS) 32 | NavigationLink(destination: PerspectiveFlippingViewExample()) { 33 | Label("PerspectiveFlippingView", systemImage: "arrow.uturn.right.square") 34 | } 35 | #endif 36 | } header: { 37 | Text("FlippingView") 38 | } 39 | } 40 | } 41 | 42 | #Preview { 43 | List { 44 | FlippingViewExamples() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FlippingViewExamples/PerspectiveFlippingViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerspectiveFlippingViewExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-09-25. 6 | // 7 | #if os(visionOS) 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct PerspectiveFlippingViewExample: View { 12 | @State private var flips: Int = 0 13 | var isFaceUp: Bool { flips.isMultiple(of: 2) } 14 | @State private var axis: Axis = .horizontal 15 | 16 | var body: some View { 17 | VStack { 18 | PerspectiveFlippingView( 19 | axis, 20 | flips: $flips 21 | ) { 22 | ZStack { 23 | Color.blue 24 | Text("Front") 25 | } 26 | .cornerRadius(20) 27 | } back: { 28 | ZStack { 29 | Color.red 30 | Text("Back") 31 | } 32 | .cornerRadius(20) 33 | } 34 | .font(.largeTitle) 35 | .padding(40) 36 | 37 | VStack { 38 | HStack { 39 | Text("Flips: \(flips)") 40 | Spacer() 41 | Text("Face \(isFaceUp ? "Up" : "Down")") 42 | } 43 | 44 | HStack { 45 | Text("Axis") 46 | Picker("Axis", selection: $axis) { 47 | ForEach(Axis.allCases, id: \.self) { axis in 48 | Text("\(axis.description)") 49 | } 50 | } 51 | .pickerStyle(.segmented) 52 | } 53 | 54 | HStack { 55 | Text("Programmatic flip") 56 | Button("-1") { flips -= 1 } 57 | Button("+1") { flips += 1 } 58 | } 59 | } 60 | .padding() 61 | } 62 | } 63 | } 64 | 65 | #Preview { 66 | FlippingViewExample() 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FlippingViewExamples/TwoSided3DViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwoSided3DViewExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-09-20. 6 | // 7 | 8 | #if os(visionOS) 9 | import SwiftUI 10 | 11 | struct TwoSided3DViewExample: View { 12 | enum ExampleAxis: Hashable, Identifiable { 13 | case horizontal 14 | case vertical 15 | case custom(_ x: CGFloat, _ y: CGFloat, _ z: CGFloat) 16 | 17 | var id: Self { self } 18 | 19 | var axis: (x: CGFloat, y: CGFloat, z: CGFloat) { 20 | switch self { 21 | case .horizontal: (0, 1, 0) 22 | case .vertical: (1, 0, 0) 23 | case let .custom(x, y, z): (x, y, z) 24 | } 25 | } 26 | } 27 | 28 | @State private var angle: Angle = .degrees(0) 29 | @State private var exampleAxis: ExampleAxis = .custom(0.2, 0.8, 0) 30 | 31 | var backView: some View { 32 | RoundedRectangle(cornerRadius: 20) 33 | .fill(.red) 34 | .overlay(Text("Down")) 35 | } 36 | 37 | var body: some View { 38 | VStack { 39 | RoundedRectangle(cornerRadius: 20) 40 | .fill(.blue) 41 | .overlay(Text("Up")) 42 | /// This modifier creates the two-sided view by supplying a rotation and a back side. 43 | .rotation3DEffect(angle, axis: exampleAxis.axis, backsideFlip: .automatic) { 44 | backView 45 | } 46 | #if os(visionOS) 47 | .frame(maxWidth: 200, maxHeight: 200) 48 | .frame(maxDepth: 200, alignment: .center) 49 | #endif 50 | .padding() 51 | 52 | Picker("Axis", selection: $exampleAxis) { 53 | ForEach([ExampleAxis.horizontal, .vertical, .custom(1.0, 1.0, 0.0), .custom(0.7, 0.3, 0)]) { exampleAxis in 54 | Text("\(String(describing: exampleAxis))") 55 | } 56 | } 57 | .pickerStyle(.segmented) 58 | 59 | Text("Change Rotation") 60 | HStack { 61 | ForEach([-360,-180,-90,-45,45,90,180,360], id: \.self) { i in 62 | Button("\(i > 0 ? "+" : "")\(i)") { 63 | withAnimation(.spring().speed(0.4)) { 64 | angle += .degrees(Double(i)) 65 | } 66 | } 67 | .padding(1) 68 | } 69 | } 70 | .padding() 71 | } 72 | } 73 | } 74 | 75 | #Preview { 76 | TwoSided3DViewExample() 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FlippingViewExamples/TwoSidedViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwoSidedViewExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-09-15. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct TwoSidedViewExample: View { 12 | enum ExampleAxis: Hashable, Identifiable { 13 | case horizontal 14 | case vertical 15 | case custom(_ x: CGFloat, _ y: CGFloat, _ z: CGFloat) 16 | 17 | var id: Self { self } 18 | 19 | var axis: (x: CGFloat, y: CGFloat, z: CGFloat) { 20 | switch self { 21 | case .horizontal: (0, 1, 0) 22 | case .vertical: (1, 0, 0) 23 | case let .custom(x, y, z): (x, y, z) 24 | } 25 | } 26 | } 27 | 28 | @State private var angle: Angle = .degrees(0) 29 | @State private var exampleAxis: ExampleAxis = .custom(0.2, 0.8, 0) 30 | 31 | var backView: some View { 32 | RoundedRectangle(cornerRadius: 20) 33 | .fill(.red) 34 | .overlay(Text("Down")) 35 | } 36 | 37 | var body: some View { 38 | VStack { 39 | RoundedRectangle(cornerRadius: 20) 40 | .fill(.blue) 41 | .overlay(Text("Up")) 42 | /// This modifier creates the two-sided view by supplying a rotation and a back side. 43 | #if os(visionOS) 44 | .perspectiveRotationEffect(angle, axis: exampleAxis.axis, backsideFlip: .automatic) { 45 | backView 46 | } 47 | #else 48 | .rotation3DEffect(angle, axis: exampleAxis.axis, perspective: 0.5, backsideFlip: .automatic) { 49 | backView 50 | } 51 | #endif 52 | .padding() 53 | 54 | 55 | Picker("Axis", selection: $exampleAxis) { 56 | ForEach([ExampleAxis.horizontal, .vertical, .custom(1.0, 1.0, 0.0), .custom(0.7, 0.3, 0)]) { exampleAxis in 57 | Text("\(String(describing: exampleAxis))") 58 | } 59 | } 60 | .pickerStyle(.segmented) 61 | 62 | Text("Change Rotation") 63 | HStack { 64 | ForEach([-360,-180,-90,-45,45,90,180,360], id: \.self) { i in 65 | Button("\(i > 0 ? "+" : "")\(i)") { 66 | withAnimation(.spring().speed(0.4)) { 67 | angle += .degrees(Double(i)) 68 | } 69 | } 70 | .padding(1) 71 | } 72 | } 73 | .padding() 74 | } 75 | } 76 | } 77 | 78 | struct TwoSidedViewExample_Previews: PreviewProvider { 79 | static var previews: some View { 80 | TwoSidedViewExample() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameAdjustmentExamples/FrameAdjustmentExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameAdjustmentExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FrameAdjustmentExamples: View { 11 | var body: some View { 12 | Section { 13 | NavigationLink(destination: WidthReaderExample()) { 14 | Label("WidthReader", systemImage: "arrow.left.and.right.square") 15 | } 16 | 17 | NavigationLink(destination: HeightReaderExample()) { 18 | Label("HeightReader", systemImage: "arrow.up.and.down.square") 19 | } 20 | 21 | NavigationLink(destination: OnSizeChangeExample()) { 22 | Label("OnSizeChange", systemImage: "arrow.up.backward.and.arrow.down.forward") 23 | } 24 | 25 | NavigationLink(destination: RelativePaddingExample()) { 26 | Label("RelativePadding", systemImage: "percent") 27 | } 28 | 29 | NavigationLink(destination: EqualWidthExample()) { 30 | Label("EqualWidth", systemImage: "arrow.left.arrow.right.square") 31 | } 32 | 33 | NavigationLink(destination: EqualHeightExample()) { 34 | Label("EqualHeight", systemImage: "arrow.up.arrow.down.square") 35 | } 36 | 37 | NavigationLink(destination: KeyboardHeightExample()) { 38 | Label("KeyboardHeight", systemImage: "keyboard") 39 | } 40 | 41 | NavigationLink(destination: ScaledToFrameExample()) { 42 | Label("ScaledToFrame", systemImage: "rectangle.and.arrow.up.right.and.arrow.down.left") 43 | } 44 | 45 | NavigationLink(destination: OverlappingImageHorizontalExample()) { 46 | Label("OverlapingImage Horizontal", systemImage: "square.righthalf.fill") 47 | } 48 | 49 | NavigationLink(destination: OverlappingImageVerticalExample()) { 50 | Label("OverlapingImage Vertical", systemImage: "square.bottomhalf.fill") 51 | } 52 | } header: { 53 | Text("Frame Adjustment") 54 | } 55 | 56 | } 57 | } 58 | 59 | struct FrameAdjustmentExamples_Previews: PreviewProvider { 60 | static var previews: some View { 61 | List { 62 | FrameAdjustmentExamples() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameAdjustmentExamples/HeightReaderExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeightReaderExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-14. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct HeightReaderExample: View { 12 | @State private var percent = 0.7 13 | 14 | var body: some View { 15 | VStack { 16 | ScrollView(.horizontal) { 17 | HeightReader { height in 18 | VStack(spacing: 0) { 19 | Text("This text frame is set to \(percent * 100, specifier: "%.0f")% of the height.") 20 | .frame(height: height * percent) 21 | .background(Color.green) 22 | 23 | Circle() 24 | } 25 | .foregroundColor(.white) 26 | .background(Color.blue) 27 | } 28 | } 29 | 30 | Text("The HeightReader above does not have a fixed width and will fit the content.") 31 | .padding() 32 | 33 | Button("Animate") { 34 | withAnimation { 35 | percent = .random(in: 0...1) 36 | } 37 | } 38 | 39 | HStack { 40 | Text("Percent \(percent * 100, specifier: "%.0f")%") 41 | #if os(tvOS) 42 | Button("-") { percent = max(0, percent - 0.1) } 43 | Button("+") { percent = min(1, percent + 0.1) } 44 | #else 45 | Slider(value: $percent, in: 0...1) 46 | #endif 47 | } 48 | .padding() 49 | } 50 | .navigationTitle("HeightReader") 51 | } 52 | } 53 | 54 | struct HeightReaderExample_Previews: PreviewProvider { 55 | static var previews: some View { 56 | HeightReaderExample() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameAdjustmentExamples/KeyboardHeightExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardHeightExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-02-02. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct KeyboardHeightExample: View { 12 | /// This environment value was added in ContentView 13 | @Environment(\.keyboardHeight) var keyboardHeight 14 | @State private var text = "" 15 | 16 | var body: some View { 17 | VStack(spacing: 0) { 18 | Text("Keyboard animation is close to the iOS keyboard movement on the iPhone but on iPad there is a delay when dismissing the keyboard. The value will always be zero on macOS, watchOS and tvOS") 19 | 20 | if #available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) { 21 | Text("Keyboard Height: \(keyboardHeight, format: .number.rounded())") 22 | .animation(nil, value: keyboardHeight) 23 | } 24 | Spacer() 25 | 26 | TextField("Moves with Keyboard", text: $text) 27 | #if os(iOS) 28 | .textFieldStyle(.roundedBorder) 29 | #endif 30 | 31 | Color.blue.opacity(0.5) 32 | .overlay(Text("Ignores Keyboard")) 33 | /// The frame height adjusts from a fixed value to the keyboard height when its visible. 34 | .frame(height: keyboardHeight == 0 ? 100 : keyboardHeight) 35 | } 36 | /// The stack needs to be at the bottom of the container 37 | .frame(maxHeight: .infinity, alignment: .bottom) 38 | /// The whole stack your view is in needs to ignore the safe area 39 | .ignoresSafeArea(.keyboard) 40 | /// You may need to add animation if environment variable was added outside a NavigationView 41 | .animation(.keyboard, value: keyboardHeight) 42 | #if !os(macOS) && !os(tvOS) 43 | .navigationBarTitleDisplayMode(.inline) 44 | #endif 45 | .navigationTitle("KeyboardHeight") 46 | } 47 | } 48 | 49 | #Preview { 50 | KeyboardHeightExample() 51 | .keyboardHeightEnvironmentValue() 52 | } 53 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameAdjustmentExamples/OnSizeChangeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnSizeChangeExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-11-22. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct OnSizeChangeExample: View { 12 | @State private var maxWidth: CGFloat = 100 13 | @State private var maxHeight: CGFloat = 100 14 | @State private var size: CGSize = .zero 15 | @State private var changes: Int = .zero 16 | 17 | var body: some View { 18 | VStack { 19 | VStack(alignment: .leading) { 20 | Text("As the view changes size, a closure runs to update the values below:") 21 | 22 | Text("Size Changes: \(changes)") 23 | .font(.headline) 24 | Text("Size: \(String(format: "%.0f", size.width)) x \(String(format: "%.0f", size.height))") 25 | .font(.headline) 26 | } 27 | .padding() 28 | 29 | Spacer() 30 | 31 | Color.blue 32 | .onSizeChange { size in 33 | self.size = size 34 | self.changes += 1 35 | } 36 | .frame(maxWidth: maxWidth, maxHeight: maxHeight) 37 | .animation(.default, value: maxWidth) 38 | .animation(.default, value: maxHeight) 39 | 40 | Spacer() 41 | 42 | VStack { 43 | HStack { 44 | #if os(tvOS) 45 | Text("Max Width \(String(format: "%.0f", maxWidth))") 46 | Button("-") { maxWidth = max(50, maxWidth - 50) } 47 | Button("+") { maxWidth = min(500, maxWidth + 50) } 48 | #else 49 | Stepper("Max Width \(String(format: "%.0f", maxWidth))", value: $maxWidth, in: 50...500, step: 50) 50 | .padding(.horizontal) 51 | #endif 52 | } 53 | 54 | HStack { 55 | #if os(tvOS) 56 | Text("Max Height \(String(format: "%.0f", maxHeight))") 57 | Button("-") { maxHeight = max(50, maxHeight - 50) } 58 | Button("+") { maxHeight = min(800, maxHeight + 50) } 59 | #else 60 | Stepper("Max Height \(String(format: "%.0f", maxHeight))", value: $maxHeight, in: 50...800, step: 50) 61 | .padding(.horizontal) 62 | #endif 63 | } 64 | } 65 | .padding() 66 | } 67 | .navigationTitle("onSizeChange") 68 | } 69 | } 70 | 71 | struct OnSizeChangeExample_Previews: PreviewProvider { 72 | static var previews: some View { 73 | NavigationView { 74 | OnSizeChangeExample() 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameAdjustmentExamples/OverlappingImageHorizontalExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlappingImageHorizonalExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-15. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct OverlappingImageHorizontalExample: View { 12 | let image = Image(systemName: "star.square") 13 | 14 | @State private var leading: CGFloat = 0.1 15 | @State private var trailing: CGFloat = 0.25 16 | 17 | var body: some View { 18 | VStack { 19 | HStack(spacing: 0) { 20 | Text("Overlapping Image") 21 | .font(.system(size: 15)) 22 | Rectangle() 23 | .frame(width: 5) 24 | 25 | OverlappingImage(image, aspectRatio: 1.0, left: leading, right: trailing) 26 | .foregroundColor(.blue.opacity(0.5)) 27 | .padding(.vertical, 50) 28 | .zIndex(1) 29 | 30 | VStack { 31 | Text("The image to the left will overlap content to the left and right with an inset based on a percent of the image width.") 32 | .padding(20) 33 | } 34 | .background(Color.gray) 35 | .padding(3) 36 | .background(Color.red) 37 | } 38 | .frame(width: 300, height: 500) 39 | 40 | VStack { 41 | HStack { 42 | Text("Leading \(leading * 100, specifier: "%.0f")%") 43 | #if os(tvOS) 44 | Button("-") { leading = max(0, leading - 0.1) } 45 | Button("+") { leading = min(1, leading + 0.1) } 46 | #else 47 | Slider(value: $leading, in: 0...1) 48 | #endif 49 | } 50 | HStack { 51 | Text("Trailing \(trailing * 100, specifier: "%.0f")%") 52 | #if os(tvOS) 53 | Button("-") { trailing = max(0, trailing - 0.1) } 54 | Button("+") { trailing = min(1, trailing + 0.1) } 55 | #else 56 | Slider(value: $trailing, in: 0...1) 57 | #endif 58 | } 59 | } 60 | .padding() 61 | } 62 | .navigationTitle("OverlapHorizontal") 63 | } 64 | } 65 | 66 | struct OverlappingImageHorizontalExample_Previews: PreviewProvider { 67 | static var previews: some View { 68 | OverlappingImageHorizontalExample() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameAdjustmentExamples/OverlappingImageVerticalExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlappingImageVerticalExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct OverlappingImageVerticalExample: View { 12 | let image = Image(systemName: "star.square") 13 | 14 | @State private var top: CGFloat = 0.1 15 | @State private var bottom: CGFloat = 0.25 16 | 17 | var body: some View { 18 | VStack { 19 | VStack(spacing: 0) { 20 | Text("Overlapping Image") 21 | .font(.system(size: 50)) 22 | Rectangle() 23 | .frame(height: 5) 24 | 25 | OverlappingImage(image, aspectRatio: 1.0, top: top, bottom: bottom) 26 | .foregroundColor(.blue.opacity(0.5)) 27 | .padding(.horizontal, 50) 28 | .zIndex(1) 29 | 30 | VStack { 31 | Text("The image above will overlap content above and below with an inset based on a percent of the image height. This allows the overlap to occur in the same location regardless of scale.") 32 | .padding(20) 33 | } 34 | .background(Color.gray) 35 | .padding(3) 36 | .background(Color.red) 37 | } 38 | .frame(width: 300, height: 500) 39 | 40 | Spacer() 41 | 42 | VStack { 43 | HStack { 44 | Text("Top \(top * 100, specifier: "%.0f")%") 45 | #if os(tvOS) 46 | Button("-") { top = max(0, top - 0.1) } 47 | Button("+") { top = min(1, top + 0.1) } 48 | #else 49 | Text("") 50 | Slider(value: $top, in: 0...1) 51 | #endif 52 | } 53 | 54 | HStack { 55 | Text("Bottom \(bottom * 100, specifier: "%.0f")%") 56 | #if os(tvOS) 57 | Button("-") { bottom = max(0, bottom - 0.1) } 58 | Button("+") { bottom = min(1, bottom + 0.1) } 59 | #else 60 | Text("") 61 | Slider(value: $bottom, in: 0...1) 62 | #endif 63 | } 64 | } 65 | .padding() 66 | } 67 | .navigationTitle("OverlapVertical") 68 | } 69 | } 70 | 71 | struct OverlappingImageVerticalExample_Previews: PreviewProvider { 72 | static var previews: some View { 73 | OverlappingImageVerticalExample() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameAdjustmentExamples/RelativePaddingExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelativePaddingExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-01-31. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct RelativePaddingExample: View { 12 | @State private var isHorizontal: Bool = true 13 | @State private var padding: CGFloat = 0.1 14 | 15 | var axis: Edge.Set { 16 | isHorizontal ? .horizontal : .vertical 17 | } 18 | 19 | var body: some View { 20 | VStack(spacing: 0) { 21 | Spacer(minLength: 0) 22 | 23 | Color.green 24 | .relativePadding(axis, padding) 25 | .background(Color.red) 26 | .padding(80) 27 | 28 | Spacer(minLength: 0) 29 | 30 | VStack(alignment: .leading) { 31 | Picker(selection: $isHorizontal) { 32 | ForEach([true, false], id: \.self) { isHorizontal in 33 | Text(isHorizontal ? "Horizontal" : "Vertical") 34 | } 35 | } label: { 36 | Text("Axis") 37 | } 38 | .pickerStyle(.segmented) 39 | 40 | HStack { 41 | Text("Padding \(padding * 100, specifier: "%.0f")%") 42 | #if os(tvOS) 43 | Button("-") { padding = max(-0.5, padding - 0.1) } 44 | Button("+") { padding = min(0.5, padding + 0.1) } 45 | #else 46 | Slider(value: $padding, in: -0.5...0.5) 47 | #endif 48 | } 49 | } 50 | .padding() 51 | } 52 | .navigationTitle("RelativePadding") 53 | } 54 | } 55 | 56 | struct RelativePaddingExample_Previews: PreviewProvider { 57 | static var previews: some View { 58 | RelativePaddingExample() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameAdjustmentExamples/WidthReaderExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidthReaderExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-06-13. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct WidthReaderExample: View { 12 | @State private var percent = 0.7 13 | @State private var padding = 0 14 | 15 | var body: some View { 16 | VStack { 17 | ScrollView { 18 | WidthReader { width in 19 | HStack(spacing: 0) { 20 | Text("This text frame is set to \(percent * 100, specifier: "%.0f")% of the width.") 21 | .frame(width: width * percent) 22 | .background(Color.green) 23 | 24 | Circle() 25 | } 26 | } 27 | .foregroundColor(.white) 28 | .background(Color.blue) 29 | } 30 | .padding(.horizontal, CGFloat(padding)) 31 | .animation(.spring(), value: padding) 32 | 33 | VStack { 34 | Text("The WidthReader above does not have a fixed height and will fit the content.") 35 | 36 | HStack { 37 | Button("Percent \(percent * 100, specifier: "%.0f")%") { 38 | withAnimation { 39 | percent = .random(in: 0...1) 40 | } 41 | } 42 | #if os(tvOS) 43 | Button("-") { percent = max(0, percent - 0.1) } 44 | Button("+") { percent = min(1, percent + 0.1) } 45 | #else 46 | Slider(value: $percent, in: 0...1) 47 | #endif 48 | } 49 | 50 | HStack { 51 | Stepper(value: $padding, in: 0...100, step: 10) { 52 | Button("Padding \(padding)") { 53 | // withAnimation { 54 | padding = Int.random(in: 0...100) 55 | // } 56 | } 57 | } 58 | } 59 | } 60 | .padding() 61 | } 62 | .navigationTitle("WidthReader") 63 | } 64 | } 65 | 66 | struct WidthReaderExample_Previews: PreviewProvider { 67 | static var previews: some View { 68 | WidthReaderExample() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Example/FrameUpExample/FrameUpExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameUpExampleApp.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct FrameUpExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Item.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Item.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-14. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Item: Identifiable { 11 | var id = UUID() 12 | var value: Value 13 | } 14 | 15 | extension Item: Sendable where Value: Sendable { } 16 | extension Item: Equatable where Value: Equatable { } 17 | extension Item: Hashable where Value: Hashable { } 18 | 19 | extension Item { 20 | static let examples = ["Here", "are", "several", "example", "items", "useful for", "creating", "example layouts", "in", "FrameUp"] 21 | .map { Item(id: UUID(), value: $0) } 22 | } 23 | 24 | extension Array> { 25 | static let examples = Element.examples 26 | } 27 | -------------------------------------------------------------------------------- /Example/FrameUpExample/LayoutExamples/CustomLayoutExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomLayoutExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-18. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 12 | struct PingPongLayout: LayoutFromFULayout { 13 | func fuLayout(maxSize: CGSize) -> PingPong { 14 | PingPong(maxWidth: maxSize.width) 15 | } 16 | } 17 | 18 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 19 | struct CustomLayoutExample: View { 20 | var body: some View { 21 | VStack { 22 | PingPongLayout { 23 | Group { 24 | Text("One") 25 | Text("Two") 26 | Text("Three") 27 | Text("Four") 28 | Text("Five") 29 | Text("Six") 30 | Text("Seven") 31 | Text("Eight") 32 | Text("Nine") 33 | Text("Ten") 34 | } 35 | .font(.title) 36 | .foregroundColor(.white) 37 | .padding(5) 38 | .background(Color.blue.cornerRadius(5)) 39 | .padding(2) 40 | } 41 | .frame(maxWidth: .infinity) 42 | 43 | Text("Easily create SwiftUI Layouts like this one from your own custom FULayout using the LayoutFromFULayout protocol.") 44 | .padding() 45 | } 46 | .navigationTitle("LayoutFromFULayout") 47 | } 48 | } 49 | 50 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 51 | struct CustomLayoutExample_Previews: PreviewProvider { 52 | static var previews: some View { 53 | NavigationStack { 54 | CustomLayoutExample() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Example/FrameUpExample/LayoutExamples/HFlowBoxLayoutExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HFlowBoxLayoutExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-05-23. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) 12 | struct HFlowBoxLayoutExample: View { 13 | @State private var boxes = [1,2,3,4,5] 14 | @State private var horizontalAlignment: FUHorizontalAlignment = .leading 15 | let verticalAlignment: FUVerticalAlignment = .top 16 | 17 | var alignment: FUAlignment { .init(horizontal: horizontalAlignment, vertical: verticalAlignment)} 18 | 19 | var body: some View { 20 | VStack { 21 | Color.blue.overlay( 22 | ScrollView { 23 | HFlowLayout(alignment: alignment) { 24 | ForEach(boxes, id: \.self) { box in 25 | Color.red 26 | .frame(width: 80, height: 80) 27 | } 28 | } 29 | } 30 | .animation(.default, value: boxes) 31 | .animation(.default, value: alignment) 32 | ) 33 | VStack { 34 | HStack { 35 | Button("Remove Box") { if !boxes.isEmpty { boxes.removeLast() } } 36 | .padding() 37 | Button("Add Box") { boxes.append((boxes.max() ?? 1) + 1) } 38 | .padding() 39 | } 40 | 41 | Picker("Horizontal Alignment", selection: $horizontalAlignment) { 42 | ForEach(FUHorizontalAlignment.allCases) { 43 | Text($0.rawValue) 44 | } 45 | } 46 | .pickerStyle(.segmented) 47 | } 48 | .padding() 49 | } 50 | .navigationTitle("HFlowLayout Boxes") 51 | } 52 | } 53 | 54 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) 55 | #Preview { 56 | HFlowBoxLayoutExample() 57 | } 58 | -------------------------------------------------------------------------------- /Example/FrameUpExample/LayoutExamples/LayoutExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LayoutExamples: View { 11 | var body: some View { 12 | Section { 13 | if #available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) { 14 | NavigationLink(destination: HFlowLayoutExample()) { 15 | Label("HFlowLayout", systemImage: "text.word.spacing") 16 | } 17 | 18 | NavigationLink(destination: HFlowBoxLayoutExample()) { 19 | Label("HFlowBoxLayout", systemImage: "square.grid.3x3") 20 | } 21 | 22 | NavigationLink(destination: VFlowLayoutExample()) { 23 | Label { 24 | Text("VFlowLayout") 25 | } icon: { 26 | Image(systemName: "text.word.spacing") 27 | .rotation3DEffect(.degrees(180), axis: (1, 1, 0)) 28 | } 29 | } 30 | 31 | NavigationLink(destination: VMasonryLayoutExample()) { 32 | Label("VMasonryLayout", systemImage: "align.vertical.top") 33 | } 34 | 35 | NavigationLink(destination: HMasonryLayoutExample()) { 36 | Label("HMasonryLayout", systemImage: "align.horizontal.left") 37 | } 38 | 39 | NavigationLink(destination: LayoutThatFitsExample()) { 40 | Label("LayoutThatFits", systemImage: "arrow.up.right.and.arrow.down.left.rectangle") 41 | } 42 | } else { 43 | UnavailableView(availableInLaterVersion: true) 44 | } 45 | } header: { 46 | Text("Layout") 47 | } 48 | } 49 | } 50 | 51 | struct LayoutExamples_Previews: PreviewProvider { 52 | static var previews: some View { 53 | List { 54 | LayoutExamples() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Example/FrameUpExample/LayoutExamples/LayoutThatFitsExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutThatFitsExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-10-29. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 12 | struct LayoutThatFitsExample: View { 13 | @State private var maxWidth: CGFloat = 200 14 | 15 | var body: some View { 16 | VStack { 17 | Spacer() 18 | 19 | VStack { 20 | Text("Above") 21 | 22 | LayoutThatFits(in: .horizontal, [HStackLayout(), VStackLayout()]) { 23 | Color.green.frame(width: 50, height: 50) 24 | Color.yellow.frame(width: 50, height: 200) 25 | Color.blue.frame(width: 50, height: 100) 26 | } 27 | .frame(width: maxWidth) 28 | .border(Color.red) 29 | 30 | Text("Below") 31 | } 32 | .animation(.default, value: maxWidth) 33 | 34 | Spacer() 35 | 36 | HStack { 37 | #if os(tvOS) 38 | Text("Max Width \(maxWidth)") 39 | Button("-") { maxWidth = max(50, maxWidth - 50) } 40 | Button("+") { maxWidth = min(350, maxWidth + 50) } 41 | #else 42 | Text("Max Width") 43 | Slider(value: $maxWidth, in: 50...350) 44 | .padding() 45 | #endif 46 | } 47 | .padding() 48 | } 49 | .navigationTitle("LayoutThatFits") 50 | } 51 | } 52 | 53 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 54 | struct LayoutThatFitsExample_Previews: PreviewProvider { 55 | static var previews: some View { 56 | NavigationStack { 57 | LayoutThatFitsExample() 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Example/FrameUpExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpExample/SmartScrollViewExamples/SmartScrollViewExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SmartScrollViewExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SmartScrollViewExamples: View { 11 | var body: some View { 12 | Section { 13 | #if os(iOS) 14 | NavigationLink(destination: SmartScrollViewExample()) { 15 | Label("SmartScroll", systemImage: "scroll") 16 | } 17 | 18 | NavigationLink(destination: SmartScrollViewSimpleExample()) { 19 | Label("SmartScrollSimple", systemImage: "scroll") 20 | } 21 | #else 22 | UnavailableView() 23 | #endif 24 | } header: { 25 | Text("SmartScrollView") 26 | } 27 | } 28 | } 29 | 30 | struct SmartScrollViewExamples_Previews: PreviewProvider { 31 | static var previews: some View { 32 | Form { 33 | SmartScrollViewExamples() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Example/FrameUpExample/SmartScrollViewExamples/SmartScrollViewSimpleExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SmartScrollViewSimpleExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-09-13. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | #if os(iOS) 12 | struct SmartScrollViewSimpleExample: View { 13 | @State private var optionalScrolling = true 14 | @State private var shrinkToFit = true 15 | @State private var maxHeight: CGFloat = 200 16 | @State private var height: CGFloat = 100 17 | 18 | var body: some View { 19 | VStack { 20 | SmartScrollView([.vertical], optionalScrolling: optionalScrolling, shrinkToFit: shrinkToFit) { 21 | Text("Content") 22 | .font(.largeTitle) 23 | .frame(height: height) 24 | .foregroundColor(.white) 25 | .padding() 26 | .background(Color.blue) 27 | } 28 | .id(maxHeight) 29 | .background(Color.red) 30 | // padding of at least 1 point is needed when inside a navigation stack as it will resize the available space 31 | .frame(maxHeight: maxHeight, alignment: .top) 32 | .padding(.top, 1) 33 | .border(Color.red) 34 | 35 | Spacer() 36 | 37 | VStack { 38 | Toggle(isOn: $optionalScrolling) { 39 | Text("Optional Scrolling") 40 | } 41 | Toggle(isOn: $shrinkToFit) { 42 | Text("Shrink to Fit") 43 | } 44 | Stepper("Content Height: \(height)", value: $height, in: 50...1000, step: 50) 45 | Stepper("Available Height: \(maxHeight)", value: $maxHeight, in: 50...1000, step: 50) 46 | } 47 | .padding() 48 | } 49 | .navigationTitle("SmartScrollViewSimple") 50 | #if os(iOS) 51 | .navigationBarTitleDisplayMode(.inline) 52 | #endif 53 | } 54 | } 55 | 56 | struct SmartScrollViewSimpleExample_Previews: PreviewProvider { 57 | static var previews: some View { 58 | SmartScrollViewSimpleExample() 59 | } 60 | } 61 | #endif 62 | -------------------------------------------------------------------------------- /Example/FrameUpExample/TabMenuExamples/TabMenuExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabMenuExampleView.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-16. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | #if os(iOS) 12 | struct TabMenuExampleView: View { 13 | @State private var selection = 0 14 | @State private var reselect: Bool = false 15 | @State private var doubleTap: Bool = false 16 | 17 | var body: some View { 18 | VStack { 19 | Group { 20 | switch selection { 21 | case 0: 22 | Color.blue 23 | .overlay(Text("Info")) 24 | case 1: 25 | Color.red 26 | .overlay(Text("Favourites")) 27 | case 2: 28 | Color.green 29 | .overlay(Text("Categories")) 30 | case 3: 31 | Color.purple 32 | .overlay(Text("About")) 33 | default: 34 | Color.white 35 | } 36 | } 37 | .font(.system(size: 30)) 38 | .overlay( 39 | VStack { 40 | Spacer() 41 | 42 | if reselect { 43 | Text("Reselect") 44 | } 45 | if doubleTap { 46 | Text("DoubleTap") 47 | } 48 | } 49 | .animation(.default, value: reselect) 50 | .animation(.default, value: doubleTap) 51 | ) 52 | .foregroundColor(.white) 53 | 54 | TabMenuExample(selection: $selection) { 55 | reselect = true 56 | } onDoubleTap: { 57 | doubleTap = true 58 | } 59 | } 60 | .onChange(of: reselect) { _ in 61 | if reselect { 62 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 63 | reselect = false 64 | } 65 | } 66 | } 67 | .onChange(of: doubleTap) { _ in 68 | if doubleTap { 69 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 70 | doubleTap = false 71 | } 72 | } 73 | } 74 | .navigationTitle("TabMenu") 75 | } 76 | } 77 | 78 | struct TabMenuViewExampleView_Previews: PreviewProvider { 79 | static var previews: some View { 80 | TabMenuExampleView() 81 | } 82 | } 83 | #endif 84 | -------------------------------------------------------------------------------- /Example/FrameUpExample/TabMenuExamples/TabMenuExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabMenuExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabMenuExamples: View { 11 | var body: some View { 12 | Section { 13 | #if os(iOS) 14 | NavigationLink(destination: TabMenuExampleView()) { 15 | Label("TabMenu", systemImage: "squares.below.rectangle") 16 | } 17 | #else 18 | UnavailableView() 19 | #endif 20 | } header: { 21 | Text("TabMenu") 22 | } 23 | } 24 | } 25 | 26 | struct TabMenuExamples_Previews: PreviewProvider { 27 | static var previews: some View { 28 | List { 29 | TabMenuExamples() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/FrameUpExample/TagViewExamples/TagViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagViewExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-15. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | @available(swift, deprecated: 6) 12 | struct TagViewExample: View { 13 | static let exampleItems = ["Thing", "Another", "Test", "Short", "Long Text is Long", "More", "Cool Tag"] 14 | @State private var items = Self.exampleItems 15 | @State private var layoutDirection: LayoutDirection = .leftToRight 16 | 17 | var body: some View { 18 | VStack { 19 | Color.clear.overlay( 20 | TagView(elements: items) { element in 21 | Text(element) 22 | .foregroundColor(.white) 23 | .padding(10) 24 | .background(Color.blue) 25 | .clipShape(Capsule()) 26 | .padding(2) 27 | } 28 | .padding(2) 29 | .background(Color.gray) 30 | .navigationTitle("TagView") 31 | .animation(.default, value: items) 32 | .animation(.default, value: layoutDirection) 33 | ) 34 | .environment(\.layoutDirection, layoutDirection) 35 | 36 | VStack { 37 | HStack { 38 | Button("Remove Item") { if !items.isEmpty { items.removeLast() } } 39 | .padding() 40 | Button("Add Item") { items.append("\(items.randomElement() ?? Self.exampleItems.randomElement()!)\(Int.random(in: 1...100))") } 41 | .padding() 42 | } 43 | 44 | Picker("Layout Direction", selection: $layoutDirection) { 45 | ForEach(LayoutDirection.allCases, id: \.self) { direction in 46 | Text(direction == .leftToRight ? "Left to Right" : "Right to Left") 47 | } 48 | } 49 | .pickerStyle(.segmented) 50 | } 51 | .padding() 52 | } 53 | .navigationTitle("TagView") 54 | } 55 | } 56 | 57 | @available(swift, deprecated: 6) 58 | struct TagViewExample_Previews: PreviewProvider { 59 | static var previews: some View { 60 | TagViewExample() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Example/FrameUpExample/TagViewExamples/TagViewExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagViewExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TagViewExamples: View { 11 | var body: some View { 12 | Section { 13 | #if swift(>=6) 14 | UnavailableView() 15 | #else 16 | NavigationLink(destination: TagViewExample()) { 17 | Label("TagView", systemImage: "tag") 18 | } 19 | 20 | NavigationLink(destination: TagViewForScrollViewExample()) { 21 | Label("TagViewForScrollView", systemImage: "tag.square") 22 | } 23 | #endif 24 | } header: { 25 | Text("TagView") 26 | } 27 | } 28 | } 29 | 30 | struct TagViewExamples_Previews: PreviewProvider { 31 | static var previews: some View { 32 | List { 33 | TagViewExamples() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Example/FrameUpExample/TagViewExamples/TagViewForScrollViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagViewForScrollViewExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-15. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | @available(swift, deprecated: 6) 12 | struct TagViewForScrollViewExample: View { 13 | static let exampleItems = ["Thing", "Another", "Test", "Short", "Long Text is Long", "More", "Cool Tag"] 14 | @State private var items = Self.exampleItems 15 | @State private var layoutDirection: LayoutDirection = .leftToRight 16 | 17 | var body: some View { 18 | VStack { 19 | Color.clear.overlay( 20 | ScrollView { 21 | WidthReader { width in 22 | Text("Some text") 23 | 24 | TagViewForScrollView(maxWidth: width, elements: items) { element in 25 | Text(element) 26 | .foregroundColor(.white) 27 | .padding(10) 28 | .background(Color.blue) 29 | .clipShape(Capsule()) 30 | .padding(2) 31 | } 32 | .padding(2) 33 | .background(Color.gray) 34 | 35 | Text("Some more text") 36 | } 37 | } 38 | .animation(.default, value: items) 39 | .animation(.default, value: layoutDirection) 40 | ) 41 | .environment(\.layoutDirection, layoutDirection) 42 | 43 | VStack { 44 | HStack { 45 | Button("Remove Item") { if !items.isEmpty { items.removeLast() } } 46 | .padding() 47 | Button("Add Item") { items.append("\(items.randomElement() ?? Self.exampleItems.randomElement()!)\(Int.random(in: 1...100))") } 48 | .padding() 49 | } 50 | 51 | Picker("Layout Direction", selection: $layoutDirection) { 52 | ForEach(LayoutDirection.allCases, id: \.self) { direction in 53 | Text(direction == .leftToRight ? "Left to Right" : "Right to Left") 54 | } 55 | } 56 | .pickerStyle(.segmented) 57 | } 58 | .padding() 59 | } 60 | .navigationTitle("TagViewForScroll") 61 | } 62 | } 63 | 64 | @available(swift, deprecated: 6) 65 | struct TagViewForScrollViewExample_Previews: PreviewProvider { 66 | static var previews: some View { 67 | NavigationView { 68 | TagViewForScrollViewExample() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Example/FrameUpExample/TextExamples/HairSpaceJustifiedTextExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HairSpaceJustifiedTextExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-10-21. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import FrameUp 10 | import SwiftUI 11 | 12 | struct HairSpaceJustifiedTextExample: View { 13 | var body: some View { 14 | VStack { 15 | /// Example of Text with mixed LTR and RTL text. 16 | Text("This ثم كلا ارتكبها إستيلاء البولندي, احداث نتيجة بالرّدأسر بـ.ثموصل جديدة aslkasjdf والكساد. ان مكثّفة العالم الهادي أضف, مما مع انذار") 17 | .font(.system(size: 16).bold()) 18 | 19 | /// Example of HairSpaceJustified with the same mixed LTR and RTL text. 20 | HairSpaceJustifiedText( 21 | // Arabic for "Hello, World" 22 | "This ثم كلا ارتكبها إستيلاء البولندي, احداث نتيجة بالرّدأسر بـ.ثموصل جديدة aslkasjdf والكساد. ان مكثّفة العالم الهادي أضف, مما مع انذار", 23 | font: .boldSystemFont(ofSize: 16), 24 | justifyLastLine: false 25 | ) 26 | 27 | HairSpaceJustifiedText( 28 | """ 29 | This is a bunch of text justified by hair spaces. SwiftUI doesn't have a way of justifying text so this view swaps out all spaces with varying numbers of hair spaces to adjust each line and make it appear justified. 30 | The last line of a paragraph is not justified by default but it can be by parameter. 31 | Line breaks continue to work as expected. 32 | 33 | Multiple line breaks remain but multiple spaces are condensed into a single space or line break. 34 | 35 | If you have a veryLongWordThatWillNotFitOnASingleLineItWillBeBrokenAtTheLastCharacterThatFits 36 | 37 | Words are not hyphenated. 38 | 39 | UIFont must be used as that is how the text width is calculated. 40 | """, 41 | font: .boldSystemFont(ofSize: 16), 42 | justifyLastLine: false 43 | ) 44 | } 45 | .padding() 46 | } 47 | } 48 | 49 | #Preview { 50 | HairSpaceJustifiedTextExample() 51 | } 52 | #endif 53 | -------------------------------------------------------------------------------- /Example/FrameUpExample/TextExamples/TextExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-09-20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TextExamples: View { 11 | var body: some View { 12 | Section { 13 | #if canImport(UIKit) 14 | NavigationLink(destination: HairSpaceJustifiedTextExample()) { 15 | Label("HairSpaceJustifiedText", systemImage: "character.textbox") 16 | } 17 | #else 18 | UnavailableView() 19 | #endif 20 | 21 | /// This check ensures this code only builds in Xcode 16+ 22 | #if compiler(>=6) 23 | if #available(iOS 18, macOS 15, watchOS 11, tvOS 18, visionOS 2, *) { 24 | NavigationLink(destination: UnclippedTextExample()) { 25 | Label("Unclipped Text", systemImage: "character.textbox") 26 | } 27 | } 28 | #endif 29 | } header: { 30 | Text("Text") 31 | } 32 | } 33 | } 34 | 35 | #Preview { 36 | TextExamples() 37 | } 38 | -------------------------------------------------------------------------------- /Example/FrameUpExample/TextExamples/UnclippedTextExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnclippedTextExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-09-20. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | /// This check ensures this code only builds in Xcode 16+ 12 | #if compiler(>=6) 13 | @available(iOS 18, macOS 15, watchOS 11, tvOS 18, visionOS 2, *) 14 | struct UnclippedTextExample: View { 15 | var body: some View { 16 | ScrollView { 17 | Grid { 18 | 19 | Text("Zapfino") 20 | .font(.custom("zapfino", size: 18)) 21 | .unclippedTextRenderer() 22 | .fixedSize() 23 | .padding() 24 | 25 | GridRow { 26 | Text("SwiftUI\nText") 27 | 28 | Text(".unclippedTextRenderer()") 29 | } 30 | .font(.caption) 31 | 32 | GridRow { 33 | Text("f") 34 | .font(.custom("zapfino", size: 30)) 35 | .border(Color.red) 36 | .frame(maxWidth: .infinity) 37 | 38 | Text("f") 39 | .font(.custom("zapfino", size: 30)) 40 | .unclippedTextRenderer() 41 | .border(Color.red) 42 | .frame(maxWidth: .infinity) 43 | } 44 | 45 | Text("System serif black italic") 46 | .font(.system(size: 20, weight: .black, design: .serif)) 47 | .padding() 48 | 49 | GridRow { 50 | Text("SwiftUI\nText") 51 | 52 | Text(".unclippedTextRenderer()") 53 | } 54 | .font(.caption) 55 | 56 | GridRow { 57 | Text("f") 58 | .font(.system(size: 70, weight: .black, design: .serif)) 59 | .italic() 60 | .border(Color.red) 61 | 62 | Text("f") 63 | .font(.system(size: 70, weight: .black, design: .serif)) 64 | .italic() 65 | .unclippedTextRenderer() 66 | .border(Color.red) 67 | } 68 | } 69 | .multilineTextAlignment(.center) 70 | .padding() 71 | } 72 | .navigationTitle("unclippedTextRenderer") 73 | } 74 | } 75 | 76 | @available(iOS 18, macOS 15, watchOS 11, tvOS 18, visionOS 2, *) 77 | #Preview { 78 | UnclippedTextExample() 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /Example/FrameUpExample/UnavailableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnavailableView.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UnavailableView: View { 11 | let availableInLaterVersion: Bool 12 | 13 | init(availableInLaterVersion: Bool = false) { 14 | self.availableInLaterVersion = availableInLaterVersion 15 | } 16 | 17 | var os: String { 18 | #if os(iOS) 19 | "iOS" 20 | #elseif os(visionOS) 21 | "visionOS" 22 | #elseif os(macOS) 23 | "macOS" 24 | #elseif os(tvOS) 25 | "tvOS" 26 | #endif 27 | } 28 | 29 | var osVersion: String { 30 | [ 31 | os, 32 | String(ProcessInfo.processInfo.operatingSystemVersion.majorVersion) 33 | ] 34 | .joined(separator: " ") 35 | } 36 | 37 | var body: some View { 38 | Label("Unavailable in \(availableInLaterVersion ? osVersion : os)", systemImage: "xmark.circle") 39 | } 40 | } 41 | 42 | struct UnavailableView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | UnavailableView() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/FrameUpExample/WidgetExamples/WidgetExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetExamples.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WidgetExamples: View { 11 | var body: some View { 12 | Section { 13 | NavigationLink(destination: WidgetSizeExample()) { 14 | Label("WidgetSize", systemImage: "questionmark.app") 15 | } 16 | 17 | NavigationLink(destination: WidgetDemoFrameExample()) { 18 | Label("WidgetDemoFrame", systemImage: "app") 19 | } 20 | 21 | #if os(iOS) 22 | NavigationLink(destination: WidgetRelativeShapeExample()) { 23 | Label("WidgetRelativeShape", systemImage: "app.dashed") 24 | } 25 | #endif 26 | } header: { 27 | Text("Widget") 28 | } 29 | } 30 | } 31 | 32 | struct WidgetExamples_Previews: PreviewProvider { 33 | static var previews: some View { 34 | List { 35 | WidgetExamples() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/FrameUpExample/WidgetExamples/WidgetRelativeShapeDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetRelativeShapeDemo.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WidgetRelativeShapeDemo: View { 11 | var body: some View { 12 | VStack { 13 | HStack { 14 | Image(systemName: "arrow.up.left") 15 | Spacer() 16 | Image(systemName: "arrow.up.right") 17 | } 18 | Spacer() 19 | Text("Widget Relative Shape (fixes iPad corner issues in iOS 15 and lower)") 20 | Spacer() 21 | HStack { 22 | Image(systemName: "arrow.down.left") 23 | Spacer() 24 | Image(systemName: "arrow.down.right") 25 | } 26 | } 27 | .padding(6) 28 | .foregroundColor(.white) 29 | .background(Color.blue) 30 | } 31 | } 32 | 33 | struct WidgetRelativeShapeDemo_Previews: PreviewProvider { 34 | static var previews: some View { 35 | WidgetRelativeShapeDemo() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Example/FrameUpExample/WidgetExamples/WidgetRelativeShapeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetRelativeShapeExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-10. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | #if os(iOS) 12 | struct WidgetRelativeShapeExample: View { 13 | var body: some View { 14 | VStack { 15 | WidgetDemoFrame(.small) { size, cornerRadius in 16 | WidgetRelativeShapeDemo() 17 | } 18 | 19 | Text("To see this example, add the example FrameUp widget.") 20 | } 21 | .padding() 22 | } 23 | } 24 | 25 | struct WidgetRelativeShapeExample_Previews: PreviewProvider { 26 | static var previews: some View { 27 | WidgetRelativeShapeExample() 28 | } 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /Example/FrameUpExample/WidgetExamples/WidgetSizeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetSizeExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-16. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct WidgetSizeExample: View { 12 | @State private var widgetSize: WidgetSize = .small 13 | 14 | var size: CGSize { 15 | #if os(iOS) 16 | widgetSize.sizeForCurrentDevice(iPadTarget: .homeScreen) ?? widgetSize.minimumSize 17 | #else 18 | widgetSize.minimumSize 19 | #endif 20 | } 21 | 22 | var sizeString: String { 23 | String(format: "%.1f", size.width) + " x " + String(format: "%.1f", size.height) 24 | } 25 | 26 | var sizes: [WidgetSize] { 27 | #if os(iOS) 28 | WidgetSize.supportedSizesForCurrentDevice 29 | #else 30 | WidgetSize.allCases 31 | #endif 32 | } 33 | 34 | var device: String { 35 | #if os(iOS) 36 | "this device" 37 | #else 38 | "the smallest possible for each widget" 39 | #endif 40 | } 41 | 42 | var body: some View { 43 | VStack { 44 | Text("Sizes below are for \(device). Widget sizes for any device can be found by supplying the screen size.") 45 | .font(.footnote) 46 | .padding() 47 | 48 | Picker("WidgetSize", selection: $widgetSize) { 49 | ForEach(sizes, id: \.self) { widgetSize in 50 | Text(widgetSize.rawValue) 51 | } 52 | } 53 | .pickerStyle(pickerStyle) 54 | .padding() 55 | 56 | Spacer(minLength: 0) 57 | 58 | Color.blue 59 | .overlay( 60 | Text(sizeString) 61 | .foregroundColor(.white) 62 | ) 63 | .frame(size) 64 | 65 | Spacer() 66 | } 67 | .navigationTitle("WidgetSize") 68 | } 69 | 70 | var pickerStyle: some PickerStyle { 71 | #if os(tvOS) 72 | .segmented 73 | #else 74 | .menu 75 | #endif 76 | } 77 | } 78 | 79 | struct WidgetSizeExample_Previews: PreviewProvider { 80 | static var previews: some View { 81 | WidgetSizeExample() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Example/FrameUpExample/_Extensions/View+IfAvailable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+IfAvailable.swift 3 | // DragAndDrop 4 | // 5 | // Created by Ryan Lintott on 2023-07-14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | /// Applies the given transform or returns the untransformed view. 12 | /// 13 | /// Useful for availability branching on view modifiers. Do not branch with any properties that may change during runtime as this will cause errors. 14 | /// - Parameters: 15 | /// - transform: The transform to apply to the source `View`. 16 | /// - Returns: The view transformed by the transform. 17 | @ViewBuilder 18 | func ifAvailable(@ViewBuilder _ transform: (Self) -> (some View)?) -> some View { 19 | if let transformed = transform(self) { 20 | transformed 21 | } else { 22 | self 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Example/FrameUpWatchExample Watch App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/FrameUpWatchExample Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-control1024.png", 5 | "idiom" : "universal", 6 | "platform" : "watchos", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Example/FrameUpWatchExample Watch App/Assets.xcassets/AppIcon.appiconset/FrameUp-icon-control1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/FrameUpWatchExample Watch App/Assets.xcassets/AppIcon.appiconset/FrameUp-icon-control1024.png -------------------------------------------------------------------------------- /Example/FrameUpWatchExample Watch App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FrameUpWatchExample Watch App/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // FrameUpWatchExample Watch App 4 | // 5 | // Created by Ryan Lintott on 2024-05-30. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | VStack { 14 | Image(.frameUpLogoAlpha) 15 | .resizable() 16 | .scaledToFit() 17 | .background(Color.white) 18 | 19 | Text("FrameUp views are not yet implemented in this example app.") 20 | } 21 | } 22 | } 23 | 24 | #Preview { 25 | ContentView() 26 | } 27 | -------------------------------------------------------------------------------- /Example/FrameUpWatchExample Watch App/FrameUpWatchExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameUpWatchExampleApp.swift 3 | // FrameUpWatchExample Watch App 4 | // 5 | // Created by Ryan Lintott on 2024-05-30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct FrameUpWatchExample_Watch_AppApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/FrameUpWatchExample Watch App/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Shared/CrossPlatform/CrossPlatformSlider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrossPlatformSlider.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-02-29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CrossPlatformSlider: View where V: BinaryFloatingPoint, V: CVarArg, V.Stride: BinaryFloatingPoint { 11 | let label: String 12 | @Binding var value: V 13 | let minValue: V 14 | let maxValue: V 15 | let step: V.Stride 16 | let decimalPlaces: Int 17 | let labelPrefix: Bool 18 | 19 | init( 20 | label: String, 21 | value: Binding, 22 | minValue: V, 23 | maxValue: V, 24 | step: V.Stride, 25 | decimalPlaces: Int = 0, 26 | labelPrefix: Bool = false 27 | ) { 28 | self.label = label 29 | self._value = value 30 | self.minValue = minValue 31 | self.maxValue = maxValue 32 | self.step = step 33 | self.decimalPlaces = decimalPlaces 34 | self.labelPrefix = labelPrefix 35 | } 36 | 37 | var format: String { 38 | "%.\(decimalPlaces)f" 39 | } 40 | 41 | var body: some View { 42 | #if os(tvOS) 43 | HStack { 44 | Text("\(label) \(String(format: format, value))") 45 | Button("-") { value = max(minValue, value.advanced(by: -step)) } 46 | Button("+") { value = min(maxValue, value.advanced(by: step)) } 47 | } 48 | #else 49 | Slider(value: $value, in: minValue...maxValue) { 50 | Text(label) 51 | } minimumValueLabel: { 52 | Text("\(labelPrefix ? "\(label): " : "")\(String(format: format, minValue))") 53 | } maximumValueLabel: { 54 | Text(String(format: format, maxValue)) 55 | } 56 | #endif 57 | } 58 | } 59 | 60 | #Preview { 61 | CrossPlatformSlider( 62 | label: "Inset", 63 | value: .constant(2.0), 64 | minValue: -30, 65 | maxValue: 30, 66 | step: 10, 67 | decimalPlaces: 0, 68 | labelPrefix: true 69 | ) 70 | .padding() 71 | } 72 | -------------------------------------------------------------------------------- /Example/Shared/CrossPlatform/CrossPlatformStepper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrossPlatformStepper.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-02-29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CrossPlatformStepper: View where V: Strideable, V: CVarArg { 11 | let label: String 12 | @Binding var value: V 13 | let minValue: V 14 | let maxValue: V 15 | let step: V.Stride 16 | let decimalPlaces: Int 17 | 18 | init( 19 | label: String, 20 | value: Binding, 21 | minValue: V, 22 | maxValue: V, 23 | step: V.Stride, 24 | decimalPlaces: Int = 0 25 | ) { 26 | self.label = label 27 | self._value = value 28 | self.minValue = minValue 29 | self.maxValue = maxValue 30 | self.step = step 31 | self.decimalPlaces = decimalPlaces 32 | } 33 | 34 | var format: String { 35 | "%.\(decimalPlaces)f" 36 | } 37 | 38 | var hStackStepper: some View { 39 | HStack { 40 | Text("\(label) \(String(format: format, value))") 41 | Button("-") { value = max(minValue, value.advanced(by: -step)) } 42 | Button("+") { value = min(maxValue, value.advanced(by: step)) } 43 | } 44 | } 45 | 46 | var body: some View { 47 | #if os(tvOS) 48 | hStackStepper 49 | #else 50 | if #available(watchOS 9.0, *) { 51 | Stepper("\(label) \(String(format: format, value))", value: $value, in: minValue...maxValue, step: step) 52 | } else { 53 | hStackStepper 54 | } 55 | #endif 56 | } 57 | } 58 | 59 | #Preview { 60 | CrossPlatformStepper( 61 | label: "Inset", 62 | value: .constant(2.0), 63 | minValue: -30, 64 | maxValue: 30, 65 | step: 10, 66 | decimalPlaces: 0 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/FrameUp-icon-control.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-icon-control.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/FrameUp-icon-control.imageset/FrameUp-icon-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/Shared/SharedAssets.xcassets/FrameUp-icon-control.imageset/FrameUp-icon-control.png -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/FrameUp-logo-alpha.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FrameUp-logo-alpha@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "FrameUp-logo-alpha@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "FrameUp-logo-alpha@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/FrameUp-logo-alpha.imageset/FrameUp-logo-alpha@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/Shared/SharedAssets.xcassets/FrameUp-logo-alpha.imageset/FrameUp-logo-alpha@1x.png -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/FrameUp-logo-alpha.imageset/FrameUp-logo-alpha@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/Shared/SharedAssets.xcassets/FrameUp-logo-alpha.imageset/FrameUp-logo-alpha@2x.png -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/FrameUp-logo-alpha.imageset/FrameUp-logo-alpha@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/FrameUp/a7576e88672196f6d9e7cda1d9e005b1fec0455e/Example/Shared/SharedAssets.xcassets/FrameUp-logo-alpha.imageset/FrameUp-logo-alpha@3x.png -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/DateTimelineProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateTimelineProvider.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-09-04. 6 | // 7 | 8 | import WidgetKit 9 | 10 | struct Provider: TimelineProvider { 11 | func placeholder(in context: Context) -> SimpleEntry { 12 | SimpleEntry(date: Date()) 13 | } 14 | 15 | func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { 16 | let entry = SimpleEntry(date: Date()) 17 | completion(entry) 18 | } 19 | 20 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 21 | var entries: [SimpleEntry] = [] 22 | 23 | // Generate a timeline consisting of five entries an hour apart, starting from the current date. 24 | let currentDate = Date() 25 | for hourOffset in 0 ..< 5 { 26 | let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! 27 | let entry = SimpleEntry(date: entryDate) 28 | entries.append(entry) 29 | } 30 | 31 | let timeline = Timeline(entries: entries, policy: .atEnd) 32 | completion(timeline) 33 | } 34 | } 35 | 36 | struct SimpleEntry: TimelineEntry { 37 | let date: Date 38 | } 39 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/FrameUpWidgetBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameUpWidgetBundle.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-09-04. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @main 12 | struct WidgetLauncher { 13 | static func main() { 14 | if #available(iOS 16, *) { 15 | FrameUpWidgetBundle.main() 16 | } else { 17 | FrameUpWidgetBundleiOS14.main() 18 | } 19 | } 20 | } 21 | 22 | @available(iOS 16, *) 23 | struct FrameUpWidgetBundle: WidgetBundle { 24 | var body: some Widget { 25 | WidgetFrameWidget() 26 | 27 | InlineImageWidget() 28 | } 29 | } 30 | 31 | struct FrameUpWidgetBundleiOS14: WidgetBundle { 32 | var body: some Widget { 33 | WidgetFrameWidget() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/InlineImageWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineImageWidget.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-09-04. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | @available(iOS 16.0, *) 12 | struct InlineImageWidget: Widget { 13 | let kind: String = "InlineImageWidget" 14 | 15 | var body: some WidgetConfiguration { 16 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 17 | InlineImageWidgetEntryView(entry: entry) 18 | } 19 | .configurationDisplayName("Inline Image Example") 20 | .description("Example of an Inline image.") 21 | .supportedFamilies([.accessoryInline]) 22 | } 23 | } 24 | 25 | @available(iOS 16.0, *) 26 | struct InlineImageWidgetEntryView: View { 27 | var entry: Provider.Entry 28 | 29 | var body: some View { 30 | InlineImageExample() 31 | } 32 | } 33 | 34 | @available(iOS 16.0, *) 35 | struct InlineImageWidget_Previews: PreviewProvider { 36 | static var previews: some View { 37 | InlineImageWidgetEntryView(entry: SimpleEntry(date: Date())) 38 | .previewContext(WidgetPreviewContext(family: .accessoryInline)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/OpenAppAppIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenAppAppIntent.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-06-20. 6 | // 7 | 8 | import Foundation 9 | import AppIntents 10 | 11 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) 12 | struct OpenApp: AppIntent { 13 | static let title: LocalizedStringResource = "Open app" 14 | 15 | @MainActor 16 | func perform() async throws -> some IntentResult { 17 | return .result() 18 | } 19 | 20 | static let openAppWhenRun: Bool = true 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/Views/InlineImageExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineImageExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-09-04. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | import WidgetKit 11 | 12 | 13 | @available(iOS 16.0, *) 14 | struct InlineImageExample: View { 15 | var body: some View { 16 | Label { 17 | Text("👈 Any Image") 18 | } icon: { 19 | AccessoryInlineImage("FrameUp-logo-alpha") 20 | } 21 | } 22 | } 23 | 24 | @available(iOS 16.0, *) 25 | struct InlineImageExample_Previews: PreviewProvider { 26 | static var previews: some View { 27 | InlineImageExample() 28 | .previewContext(WidgetPreviewContext(family: .accessoryInline)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/Views/JustifiedTextExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JustifiedTextExample.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-11-15. 6 | // 7 | 8 | import FrameUp 9 | import SwiftUI 10 | import WidgetKit 11 | 12 | struct JustifiedTextExample: View { 13 | let text: String 14 | var words: Array<(offset: Int, element: String)> { 15 | Array(text.split(separator: " ").map(String.init).enumerated()) 16 | } 17 | 18 | var body: some View { 19 | GeometryReader { proxy in 20 | HFlow(alignment: .topJustified, maxWidth: proxy.size.width) { 21 | ForEach(words, id: \.offset) { word in 22 | Text(word.element) 23 | } 24 | } 25 | } 26 | .padding() 27 | } 28 | } 29 | 30 | struct JustifiedTextExample_Previews: PreviewProvider { 31 | static var previews: some View { 32 | JustifiedTextExample(text: "Hello World! Here is some long text that is justified using HFlow from FrameUp. It seems to work ok even with a bunch of text.") 33 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/Views/WidgetRelativeShapeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetRelativeShape.swift 3 | // FrameUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-11-24. 6 | // 7 | 8 | #if os(iOS) 9 | import FrameUp 10 | import SwiftUI 11 | import WidgetKit 12 | 13 | @available(iOS, obsoleted: 16, message: "This modifier is no longer needed") 14 | struct WidgetRelativeShapeExample: View { 15 | var body: some View { 16 | WidgetRelativeShapeDemo() 17 | .clipShape(WidgetRelativeShape(.systemSmall)) 18 | .background( 19 | ContainerRelativeShape() 20 | .fill(.red) 21 | ) 22 | .padding(1) 23 | } 24 | } 25 | 26 | @available(iOS, obsoleted: 16, message: "This modifier is no longer needed") 27 | struct WidgetRelativeShape_Previews: PreviewProvider { 28 | static var previews: some View { 29 | Group { 30 | if #available(iOS 16, *) { 31 | 32 | } else { 33 | // broken on iPads 34 | ContainerRelativeShape() 35 | .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) 36 | .foregroundColor(.red) 37 | .overlay( 38 | Text("Corner radius not correct on iPad widgets in iOS 15 and lower") 39 | ) 40 | .padding(1) 41 | } 42 | 43 | // fixed on all devices 44 | WidgetRelativeShapeExample() 45 | } 46 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /Example/WidgetFrameWidget/WidgetFrameWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetFrameWidget.swift 3 | // WidgetFrameWidget 4 | // 5 | // Created by Ryan Lintott on 2021-11-23. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | struct WidgetFrameWidget: Widget { 12 | let kind: String = "WidgetFrameWidget" 13 | 14 | var body: some WidgetConfiguration { 15 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 16 | WidgetFrameWidgetEntryView(entry: entry) 17 | } 18 | .configurationDisplayName("WidgetFrame Example") 19 | .description("Example of WidgetFrame.") 20 | } 21 | } 22 | 23 | struct WidgetFrameWidgetEntryView: View { 24 | var entry: Provider.Entry 25 | 26 | var body: some View { 27 | WidgetRelativeShapeExample() 28 | } 29 | } 30 | 31 | struct WidgetFrameWidget_Previews: PreviewProvider { 32 | static var previews: some View { 33 | WidgetFrameWidgetEntryView(entry: SimpleEntry(date: Date())) 34 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ryan Lintott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "FrameUp", 8 | platforms: [ 9 | .iOS(.v14), 10 | .macOS(.v11), 11 | .watchOS(.v7), 12 | .tvOS(.v14), 13 | .visionOS(.v1) 14 | ], 15 | products: [ 16 | // Products define the executables and libraries a package produces, and make them visible to other packages. 17 | .library( 18 | name: "FrameUp", 19 | targets: ["FrameUp"] 20 | ), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "FrameUp", 27 | resources: [.copy("PrivacyInfo.xcprivacy")] 28 | ), 29 | .testTarget( 30 | name: "FrameUpTests", 31 | dependencies: ["FrameUp"] 32 | ), 33 | ], 34 | swiftLanguageVersions: [.v5, .version("6")] 35 | ) 36 | -------------------------------------------------------------------------------- /Sources/FrameUp/AutoRotatingView/FUInterfaceOrientation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterfaceOrientation-extension.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-05-23. 6 | // 7 | 8 | #if os(iOS) 9 | import SwiftUI 10 | 11 | public enum FUInterfaceOrientation: CaseIterable, Sendable { 12 | case portrait 13 | case landscapeRight 14 | case landscapeLeft 15 | case portraitUpsideDown 16 | } 17 | 18 | internal extension FUInterfaceOrientation { 19 | init?(key: String) { 20 | switch key { 21 | case "UIInterfaceOrientationPortrait": 22 | self = .portrait 23 | case "UIInterfaceOrientationLandscapeLeft": 24 | /// UIInterfaceOrientationLandscapeLeft means the interface has turned to the LEFT even though the device has turned to the RIGHT. 25 | self = .landscapeRight 26 | case "UIInterfaceOrientationLandscapeRight": 27 | /// UIInterfaceOrientationLandscapeLeft means the interface has turned to the RIGHT even though the device has turned to the LEFT. 28 | self = .landscapeLeft 29 | case "UIInterfaceOrientationPortraitUpsideDown": 30 | self = .portraitUpsideDown 31 | default: 32 | return nil 33 | } 34 | } 35 | 36 | var isLandscape: Bool { 37 | switch self { 38 | case .landscapeLeft, .landscapeRight: 39 | return true 40 | default: 41 | return false 42 | } 43 | } 44 | 45 | /// The rotation angle required to change this orientation and a new orientation. 46 | func rotation(to newOrientation: Self) -> Angle { 47 | switch (self, newOrientation) { 48 | case (.portrait, .landscapeLeft), (.landscapeLeft, .portraitUpsideDown), (.portraitUpsideDown, .landscapeRight), (.landscapeRight, .portrait): 49 | return .degrees(-90) 50 | case (.portrait, .landscapeRight), (.landscapeRight, .portraitUpsideDown), (.portraitUpsideDown, .landscapeLeft), (.landscapeLeft, .portrait): 51 | return .degrees(90) 52 | case (.portrait, .portraitUpsideDown), (.landscapeRight, .landscapeLeft), (.portraitUpsideDown, .portrait), (.landscapeLeft, .landscapeRight): 53 | return .degrees(180) 54 | default: 55 | return .zero 56 | } 57 | } 58 | 59 | var name: String { 60 | switch self { 61 | case .portrait: 62 | return "portrait" 63 | case .landscapeLeft: 64 | return "landscapeLeft" 65 | case .landscapeRight: 66 | return "landscapeRight" 67 | case .portraitUpsideDown: 68 | return "portraitUpsideDown" 69 | } 70 | } 71 | } 72 | 73 | @available(iOS 15, * ) 74 | internal extension FUInterfaceOrientation { 75 | init?(_ interfaceOrientation: InterfaceOrientation) { 76 | switch interfaceOrientation { 77 | case .landscapeLeft: self = .landscapeLeft 78 | case .landscapeRight: self = .landscapeRight 79 | case .portrait: self = .portrait 80 | case .portraitUpsideDown: self = .portraitUpsideDown 81 | default: return nil 82 | } 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /Sources/FrameUp/AutoRotatingView/InfoDictionary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfoDictionary.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-05-11. 6 | // 7 | 8 | #if os(iOS) 9 | import SwiftUI 10 | 11 | enum InfoDictionary { 12 | static let supportedInterfaceOrientations: [FUInterfaceOrientation] = { 13 | if let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations"] as? [String] { 14 | return orientations.compactMap { FUInterfaceOrientation(key: $0) } 15 | } else { 16 | return [] 17 | } 18 | }() 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /Sources/FrameUp/AutoRotatingView/RotationMatchingOrientationViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RotationMatchingOrientationViewModifier.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2020-12-31. 6 | // 7 | 8 | #if os(iOS) 9 | import SwiftUI 10 | 11 | extension View { 12 | /// Rotates a view and alters it's frame to match device orientations from an allowed orientation set. 13 | /// 14 | /// View will use a GeometryReader to take all the available space. 15 | /// - Parameters: 16 | /// - allowedOrientations: Set of allowed orientations for this view. Default is all. 17 | /// - isOn: Toggle to turn this modifier on or off. 18 | /// - animation: Animation to use when altering the view orientation. 19 | /// - Returns: A view rotated to match a device orientations from an allowed orientation set. 20 | @available(*, deprecated, renamed: "AutoRotatingView", message: "Use AutoRotatingView view instead of this modifier.") 21 | public func rotationMatchingOrientation(_ allowedOrientations: [FUInterfaceOrientation]? = nil, isOn: Bool = true, withAnimation animation: Animation? = nil) -> some View { 22 | AutoRotatingView(allowedOrientations ?? FUInterfaceOrientation.allCases, isOn: isOn, animation: animation) { 23 | self 24 | } 25 | } 26 | } 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/FrameUp/AutoRotatingView/UIDeviceOrientation-extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceOrientation-extension.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-05-14. 6 | // 7 | 8 | #if os(iOS) 9 | import SwiftUI 10 | 11 | internal extension UIDeviceOrientation { 12 | var interfaceOrientation: FUInterfaceOrientation? { 13 | switch self { 14 | case .portrait: .portrait 15 | case .portraitUpsideDown: .portraitUpsideDown 16 | case .landscapeLeft: .landscapeLeft 17 | case .landscapeRight: .landscapeRight 18 | default: nil 19 | } 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/FrameUp/FULayout/AnyFULayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyFULayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-07-18. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A type-erased instance of ``FULayout``. 12 | 13 | If you want to make a view that can toggle between layouts, wrap each one in `AnyFULayout`. 14 | */ 15 | public struct AnyFULayout: FULayout { 16 | /// The name of the wrapped layout (just used as a label) 17 | public let fuLayoutName: String 18 | public let layoutHash: Int 19 | public let fixedSize: Axis.Set 20 | public let maxItemWidth: CGFloat? 21 | public let maxItemHeight: CGFloat? 22 | /// A closure that holds the layout function for the wrapped layout. 23 | private let contentOffsets: ([Int : CGSize]) -> [Int : CGPoint] 24 | 25 | /// Creates a type-erased FrameUp layout. 26 | /// - Parameter layout: FrameUp layout that will be type-erased. 27 | public init(_ layout: L) { 28 | fuLayoutName = String(describing: L.self) 29 | fixedSize = layout.fixedSize 30 | maxItemWidth = layout.maxItemWidth 31 | maxItemHeight = layout.maxItemHeight 32 | contentOffsets = layout.contentOffsets 33 | layoutHash = layout.hashValue 34 | } 35 | 36 | public func contentOffsets(sizes: [Int: CGSize]) -> [Int: CGPoint] { 37 | contentOffsets(sizes) 38 | } 39 | 40 | public static func == (lhs: AnyFULayout, rhs: AnyFULayout) -> Bool { 41 | lhs.fuLayoutName == rhs.fuLayoutName 42 | && lhs.layoutHash == rhs.layoutHash 43 | && lhs.fixedSize == rhs.fixedSize 44 | && lhs.maxItemWidth == rhs.maxItemWidth 45 | && lhs.maxItemHeight == rhs.maxItemHeight 46 | } 47 | 48 | public func hash(into hasher: inout Hasher) { 49 | /// Use the same hash as the inherited layout 50 | hasher.combine(layoutHash) 51 | /// Adding a string of this type name will differentiate AnyFULayout(layout) from layout 52 | hasher.combine(String(describing: Self.self)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/FrameUp/FULayout/FULayout+forEach.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FULayout+forEach.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-05-31. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension FULayout { 11 | /// Creates a FrameUp layout view by computing views on demand from an underlying collection of identified data. 12 | /// - Parameters: 13 | /// - data: Data used to generate views. 14 | /// - content: Closure that takes one item from data and generates a view for that item. 15 | /// - Returns: A views on demand from an underlying collection of identified data. 16 | @available(iOS, introduced: 14, deprecated: 16, message: "FULayout can be replaced with SwiftUI Layout equivalent. For example: HFlow -> HFlowLayout") 17 | @available(macOS, introduced: 11, deprecated: 13, message: "FULayout can be replaced with SwiftUI Layout equivalent. For example: HFlow -> HFlowLayout") 18 | @available(watchOS, introduced: 7, deprecated: 9, message: "FULayout can be replaced with SwiftUI Layout equivalent. For example: HFlow -> HFlowLayout") 19 | @available(tvOS, introduced: 14, deprecated: 16, message: "FULayout can be replaced with SwiftUI Layout equivalent. For example: HFlow -> HFlowLayout") 20 | @available(visionOS, introduced: 1, deprecated: 1, message: "FULayout can be replaced with SwiftUI Layout equivalent. For example: HFlow -> HFlowLayout") 21 | @preconcurrency @MainActor 22 | func forEach(_ data: Data, content: @escaping (Data.Element) -> Content) -> some View where Data.Element: Identifiable, Data.Index == Int { 23 | FULayoutEach(data, layout: self, content: content) 24 | } 25 | } 26 | 27 | @available(iOS, introduced: 14, deprecated: 16) 28 | @available(macOS, introduced: 11, deprecated: 13) 29 | @available(watchOS, introduced: 7, deprecated: 9) 30 | @available(tvOS, introduced: 14, deprecated: 16) 31 | @available(visionOS, introduced: 1, deprecated: 1) 32 | fileprivate struct FULayoutEach: View where Data.Element: Identifiable, Data.Index == Int { 33 | let data: Array<(Data.Element, Int)> 34 | let layout: L 35 | let content: (Data.Element) -> Content 36 | 37 | @State private var contentOffsets: [Int: CGPoint] = [:] 38 | @State private var frameSize: CGSize? = nil 39 | 40 | init(_ data: Data, layout: L, content: @escaping (Data.Element) -> Content) { 41 | self.data = Array(zip(data, data.indices)) 42 | self.layout = layout 43 | self.content = content 44 | } 45 | 46 | var defaultOffset: CGPoint { 47 | contentOffsets.first?.value ?? .zero 48 | } 49 | 50 | var body: some View { 51 | FULayoutRootView(layout, contentOffsets: $contentOffsets, frameSize: $frameSize) { 52 | ForEach(data, id: \.0.id) { (item, index) in 53 | FULayoutChildView(layout: layout, index: index, contentOffset: contentOffsets[index], defaultOffset: defaultOffset, content: content(item)) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/FrameUp/FULayout/FULayoutSizeKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FULayoutSizeKey.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A preference key used for managing view sizes in an ``FULayout`` view. 11 | public struct FULayoutSizeKey: PreferenceKey { 12 | public typealias Value = [Int: CGSize] 13 | public static let defaultValue: [Int: CGSize] = [:] 14 | public static func reduce(value: inout Value, nextValue: () -> Value) { 15 | nextValue().forEach { 16 | value.updateValue($0.value, forKey: $0.key) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/FrameUp/FULayout/FULayouts/HStackFULayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HStackFULayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-07-14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A FrameUp layout version of `HStackLayout`. Useful when you want to toggle between different FrameUp layouts. 11 | @available(iOS, introduced: 14, deprecated: 16, message: "HStackFULayout can be replaced with SwiftUI HStackLayout") 12 | @available(macOS, introduced: 11, deprecated: 13, message: "HStackFULayout can be replaced with SwiftUI HStackLayout") 13 | @available(watchOS, introduced: 7, deprecated: 9, message: "HStackFULayout can be replaced with SwiftUI HStackLayout") 14 | @available(tvOS, introduced: 14, deprecated: 16, message: "HStackFULayout can be replaced with SwiftUI HStackLayout") 15 | @available(visionOS, introduced: 1, deprecated: 1, message: "HStackFULayout can be replaced with SwiftUI HStackLayout") 16 | public struct HStackFULayout: FULayout, Sendable { 17 | typealias Row = FULayoutRow 18 | 19 | public let alignment: FUVerticalAlignment 20 | public let spacing: CGFloat 21 | public let maxHeight: CGFloat 22 | public let maxItemWidth: CGFloat? 23 | 24 | public var maxItemHeight: CGFloat? { maxHeight } 25 | public let fixedSize: Axis.Set = .horizontal 26 | 27 | /// Creates a FrameUp layout version of `HStackLayout`. 28 | /// - Parameters: 29 | /// - alignment: Vertical alignment of elements. 30 | /// - spacing: Minimum horizontal spacing between views. Default is 10 31 | /// - maxHeight: Maximum height (can be obtained through a `HeightReader`). 32 | /// - maxItemWidth: Maximum width for each child view. Default is infinity. 33 | public init( 34 | alignment: FUVerticalAlignment = .center, 35 | spacing: CGFloat? = nil, 36 | maxHeight: CGFloat, 37 | maxItemWidth: CGFloat? = nil 38 | ) { 39 | self.alignment = alignment.replacingJustification() 40 | self.spacing = spacing ?? 10 41 | self.maxHeight = maxHeight 42 | self.maxItemWidth = maxItemWidth 43 | } 44 | 45 | public func contentOffsets(sizes: [Int: CGSize]) -> [Int: CGPoint] { 46 | var row = Row(alignment: .init(horizontal: .leading, vertical: alignment), minSpacing: spacing) 47 | 48 | sizes.forEach { row.append($0) } 49 | 50 | return row.contentOffsets(rowYOffset: 0) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/FrameUp/FULayout/FULayouts/VStackFULayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStackFULayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-07-14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A FrameUp layout version of `VStackLayout`. Useful when you want to toggle between different FrameUp layouts. 11 | @available(iOS, introduced: 14, deprecated: 16, message: "VStackFULayout can be replaced with SwiftUI VStackLayout") 12 | @available(macOS, introduced: 11, deprecated: 13, message: "VStackFULayout can be replaced with SwiftUI VStackLayout") 13 | @available(watchOS, introduced: 7, deprecated: 9, message: "VStackFULayout can be replaced with SwiftUI VStackLayout") 14 | @available(tvOS, introduced: 14, deprecated: 16, message: "VStackFULayout can be replaced with SwiftUI VStackLayout") 15 | @available(visionOS, introduced: 1, deprecated: 1, message: "VStackFULayout can be replaced with SwiftUI VStackLayout") 16 | public struct VStackFULayout: FULayout, Sendable { 17 | typealias Column = FULayoutColumn 18 | 19 | public let alignment: FUHorizontalAlignment 20 | public let spacing: CGFloat 21 | public let maxWidth: CGFloat 22 | public let maxItemHeight: CGFloat? 23 | 24 | public var maxItemWidth: CGFloat? { maxWidth } 25 | public let fixedSize: Axis.Set = .vertical 26 | 27 | /// Creates a FrameUp layout version of `HStackLayout`. 28 | /// - Parameters: 29 | /// - alignment: Horizontal alignment of elements. 30 | /// - spacing: Minimum vertical spacing between views. Default is 10 31 | /// - maxWidth: Maximum width (can be obtained through a `WidthReader`). 32 | /// - maxItemHeight: Maximum height for each child view. Default is infinity. 33 | public init( 34 | alignment: FUHorizontalAlignment = .center, 35 | spacing: CGFloat? = nil, 36 | maxWidth: CGFloat, 37 | maxItemHeight: CGFloat? = nil 38 | ) { 39 | self.alignment = alignment.replacingJustification() 40 | self.spacing = spacing ?? 10 41 | self.maxWidth = maxWidth 42 | self.maxItemHeight = maxItemHeight 43 | } 44 | 45 | public func contentOffsets(sizes: [Int: CGSize]) -> [Int: CGPoint] { 46 | var column = Column(alignment: .init(horizontal: alignment, vertical: .top), minSpacing: spacing) 47 | 48 | sizes.forEach { column.append($0) } 49 | 50 | return column.contentOffsets(columnXOffset: 0) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/FrameUp/FULayout/FULayouts/ZStackFULayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZStackFULayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-07-14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A FrameUp layout version of `ZStackLayout`. Useful when you want to toggle between different FrameUp layouts. 11 | @available(iOS, introduced: 14, deprecated: 16, message: "ZStackFULayout can be replaced with SwiftUI ZStackLayout") 12 | @available(macOS, introduced: 11, deprecated: 13, message: "ZStackFULayout can be replaced with SwiftUI ZStackLayout") 13 | @available(watchOS, introduced: 7, deprecated: 9, message: "ZStackFULayout can be replaced with SwiftUI ZStackLayout") 14 | @available(tvOS, introduced: 14, deprecated: 16, message: "ZStackFULayout can be replaced with SwiftUI ZStackLayout") 15 | @available(visionOS, introduced: 1, deprecated: 1, message: "ZStackFULayout can be replaced with SwiftUI ZStackLayout") 16 | public struct ZStackFULayout: FULayout, Sendable { 17 | public let alignment: FUAlignment 18 | public let maxWidth: CGFloat 19 | public let maxHeight: CGFloat 20 | 21 | public var maxItemWidth: CGFloat? { maxWidth } 22 | public var maxItemHeight: CGFloat? { maxHeight } 23 | public var itemAlignment: FUAlignment { alignment } 24 | public let fixedSize: Axis.Set = [] 25 | 26 | /// Creates a FrameUp layout version of `ZStackLayout`. 27 | /// - Parameters: 28 | /// - alignment: Alignment for elements. 29 | /// - maxWidth: Maximum width. 30 | /// - maxHeight: Maximum height. 31 | public init( 32 | alignment: FUAlignment? = nil, 33 | maxWidth: CGFloat, 34 | maxHeight: CGFloat 35 | ) { 36 | self.alignment = alignment?.replacingVerticalJustification().replacingHorizontalJustification() ?? .center 37 | self.maxWidth = maxWidth 38 | self.maxHeight = maxHeight 39 | } 40 | 41 | public func contentOffsets(sizes: [Int : CGSize]) -> [Int : CGPoint] { 42 | var result = [Int: CGPoint]() 43 | 44 | for size in sizes.sortedByKey() { 45 | let xOffset: CGFloat 46 | switch alignment.horizontal { 47 | case .leading, .justified: 48 | xOffset = .zero 49 | case .center: 50 | xOffset = -size.value.width / 2 51 | case .trailing: 52 | xOffset = -size.value.width 53 | } 54 | let yOffset: CGFloat 55 | switch alignment.vertical { 56 | case .top, .justified: 57 | yOffset = .zero 58 | case .center: 59 | yOffset = -size.value.height / 2 60 | case .bottom: 61 | yOffset = -size.value.height 62 | } 63 | let offset = CGPoint(x: xOffset, y: yOffset) 64 | 65 | result.updateValue(offset, forKey: size.key) 66 | } 67 | 68 | return result 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/FrameUp/FrameAdjustment/OverlappingImage/OverlappingImage+UIImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlappingImage+UIImage.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-01-25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if canImport(UIKit) 11 | extension OverlappingImage { 12 | /// Creates an image view that overlaps content at the edges of its frame 13 | /// - Parameters: 14 | /// - uiImage: Image that will overlap content. 15 | /// - top: Overlap percent at top edge. 16 | /// - bottom: Overlap percent at bottom edge. 17 | public init(uiImage: UIImage, top: CGFloat = 0, bottom: CGFloat = 0) { 18 | self.init(Image(uiImage: uiImage), aspectRatio: uiImage.size.aspectRatio, top: top, bottom: bottom) 19 | } 20 | 21 | /// Creates an image view that overlaps content at the edges of its frame 22 | /// - Parameters: 23 | /// - uiImage: Image that will overlap content. 24 | /// - left: Overlap percent at left edge. 25 | /// - right: Overlap percent at right edge. 26 | public init(uiImage: UIImage, left: CGFloat = 0, right: CGFloat = 0) { 27 | self.init(Image(uiImage: uiImage), aspectRatio: uiImage.size.aspectRatio, left: left, right: right) 28 | } 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/FrameUp/FrameAdjustment/Proportionable/AspectFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AspectFormat.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-11. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An enumeration to indicate the aspect format of a frame. 11 | /// 12 | /// Used in ``Proportionable`` protocol 13 | public enum AspectFormat: CaseIterable, Sendable { 14 | case portrait, square, landscape 15 | 16 | /// The aspect ratio format for a given aspect ratio 17 | /// - Parameter aspectRatio: Aspect ratio (width / height) 18 | /// - Returns: Aspect ratio format 19 | public static func forRatio(_ aspectRatio: CGFloat) -> Self { 20 | switch aspectRatio { 21 | case 1: 22 | return .square 23 | case ..<1: 24 | return .portrait 25 | default: 26 | return .landscape 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/FrameUp/FrameAdjustment/Readers/HeightReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeightReader.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-06-11. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Preference key used to pass the height of a child view up the hierarchy. 11 | /// 12 | /// Used by ``HeightReader``. 13 | /// 14 | /// Only one key is necessary and works even in nested situations because the value is captured and used inside reader view. Nested views will replace the value before reading it so the correct value should always be sent through. 15 | public struct HeightKey: PreferenceKey { 16 | public typealias Value = CGFloat 17 | public static let defaultValue: CGFloat = .zero 18 | public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 19 | value = nextValue() 20 | } 21 | } 22 | 23 | /// A 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. 24 | /// 25 | /// Useful inside horizontal scroll views where you want to measure the height without specifying a frame width. 26 | public struct HeightReader: View { 27 | let alignment: VerticalAlignment 28 | @ViewBuilder let content: (CGFloat) -> Content 29 | 30 | @State private var height: CGFloat = 0 31 | 32 | /// Creates a view takes the available height and provides this measurement to its content. 33 | /// - Parameters: 34 | /// - alignment: Vertical alignment 35 | /// - content: any `View` 36 | public init(alignment: VerticalAlignment = .top, @ViewBuilder content: @escaping (CGFloat) -> Content) { 37 | self.alignment = alignment 38 | self.content = content 39 | } 40 | 41 | @ViewBuilder 42 | public var elements: some View { 43 | Color.clear.overlay( 44 | GeometryReader { proxy in 45 | Color.clear 46 | .preference(key: HeightKey.self, value: proxy.size.height) 47 | } 48 | ) 49 | .frame(width: 0) 50 | .onPreferenceChangeMainActor(HeightKey.self) { newHeight in 51 | if height == newHeight { return } 52 | height = newHeight 53 | } 54 | 55 | /// Only show the content if the height has been set (is not zero) 56 | if height > 0 { 57 | content(height) 58 | } 59 | } 60 | 61 | public var body: some View { 62 | /// This extra HStack is here because trying to apply frame modifiers to HeightReader may not work correctly without it. 63 | HStack { 64 | if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { 65 | FittedHStack(alignment: .init(alignment) ?? .center) { 66 | elements 67 | } 68 | } else { 69 | HStack(alignment: alignment, spacing: 0) { 70 | elements 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/FrameUp/FrameAdjustment/Readers/KeyboardHeightEnvironmentValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardHeightEnvironmentValue.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2024-02-01. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | private struct KeyboardHeightEnvironmentKey: EnvironmentKey { 12 | static let defaultValue: CGFloat = 0 13 | } 14 | 15 | public extension EnvironmentValues { 16 | /// Height of software keyboard when visible 17 | var keyboardHeight: CGFloat { 18 | get { self[KeyboardHeightEnvironmentKey.self] } 19 | set { self[KeyboardHeightEnvironmentKey.self] = newValue } 20 | } 21 | } 22 | 23 | public extension Animation { 24 | /// An approximation of Apple's keyboard animation 25 | /// 26 | /// source: https://forums.developer.apple.com/forums/thread/48088 27 | static var keyboard: Self { 28 | .interpolatingSpring(mass: 3, stiffness: 1000, damping: 500, initialVelocity: 0) 29 | } 30 | } 31 | 32 | #if os(iOS) 33 | struct KeyboardHeightEnvironmentValue: ViewModifier { 34 | @State private var keyboardHeight: CGFloat = 0 35 | 36 | func body(content: Content) -> some View { 37 | content 38 | .environment(\.keyboardHeight, keyboardHeight) 39 | .animation(.keyboard, value: keyboardHeight) 40 | .background( 41 | GeometryReader { keyboardProxy in 42 | GeometryReader { proxy in 43 | Color.clear 44 | .onChange(of: keyboardProxy.safeAreaInsets.bottom - proxy.safeAreaInsets.bottom) { newValue in 45 | DispatchQueue.main.async { 46 | if keyboardHeight != newValue { 47 | keyboardHeight = newValue 48 | } 49 | } 50 | } 51 | } 52 | .ignoresSafeArea(.keyboard) 53 | } 54 | ) 55 | } 56 | } 57 | #endif 58 | 59 | public extension View { 60 | /// Adds an environment value for software keyboard height when visible 61 | /// 62 | /// Must be applied on a view taller than the keyboard that touches the bottom edge of the safe area. 63 | /// Access keyboard height in any child view with 64 | /// @Environment(\.keyboardHeight) var keyboardHeight 65 | func keyboardHeightEnvironmentValue() -> some View { 66 | #if os(iOS) 67 | modifier(KeyboardHeightEnvironmentValue()) 68 | #else 69 | environment(\.keyboardHeight, 0) 70 | #endif 71 | } 72 | } 73 | 74 | #Preview { 75 | VStack { 76 | TextField("Example", text: .constant("")) 77 | } 78 | .keyboardHeightEnvironmentValue() 79 | } 80 | -------------------------------------------------------------------------------- /Sources/FrameUp/FrameAdjustment/Readers/OnSizeChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnSizeChange.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-11-22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Used by `onSizeChange` 11 | /// 12 | /// Only one key is necessary and works even in nested situations because the value is captured and used inside reader view. 13 | /// Nested views will replace the value before reading it so the correct value should always be sent through. 14 | public struct SizeKey: PreferenceKey { 15 | public typealias Value = CGSize 16 | public static let defaultValue: CGSize = .zero 17 | public static func reduce(value: inout CGSize, nextValue: () -> CGSize) { 18 | value = nextValue() 19 | } 20 | } 21 | 22 | public extension View { 23 | /// Adds an action to perform when parent view size value changes. This action will also run when the view first appears to send the initial size. 24 | /// - Parameter action: The action to perform when the size changes. The action closure passes the new value as its parameter. 25 | /// - Returns: A view with an invisible background `GeometryReader` that detects and triggers an action when the size changes. 26 | @preconcurrency func onSizeChange(perform action: @escaping @MainActor (CGSize) -> Void) -> some View { 27 | self 28 | .background( 29 | GeometryReader { proxy in 30 | Color.clear 31 | .preference(key: SizeKey.self, value: proxy.size) 32 | } 33 | ) 34 | .onPreferenceChangeMainActor(SizeKey.self, perform: action) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/FrameUp/FrameAdjustment/Readers/WidthReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidthReader.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-06-10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Preference key used to pass the width of a child view up the hierarchy. 11 | /// 12 | /// Used by `WidthReader`. 13 | /// 14 | /// Only one key is necessary and works even in nested situations because the value is captured and used inside reader view. Nested views will replace the value before reading it so the correct value should always be sent through. 15 | public struct WidthKey: PreferenceKey { 16 | public typealias Value = CGFloat 17 | public static let defaultValue: CGFloat = .zero 18 | public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 19 | value = nextValue() 20 | } 21 | } 22 | 23 | /// A 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. 24 | /// 25 | /// Useful inside vertical scroll views where you want to measure the width without specifying a frame height. 26 | public struct WidthReader: View { 27 | let alignment: HorizontalAlignment 28 | @ViewBuilder let content: (CGFloat) -> Content 29 | 30 | @State private var width: CGFloat = 0 31 | 32 | /// Creates a view takes the available width and provides this measurement to its content. 33 | /// - Parameters: 34 | /// - alignment: Horizontal alignment 35 | /// - content: any `View` 36 | public init(alignment: HorizontalAlignment = .center, @ViewBuilder content: @escaping (CGFloat) -> Content) { 37 | self.alignment = alignment 38 | self.content = content 39 | } 40 | 41 | @ViewBuilder 42 | public var elements: some View { 43 | Color.clear.overlay( 44 | GeometryReader { proxy in 45 | Color.clear 46 | .preference(key: WidthKey.self, value: proxy.size.width) 47 | } 48 | ) 49 | .frame(height: 0) 50 | .onPreferenceChangeMainActor(WidthKey.self) { newWidth in 51 | if width == newWidth { return } 52 | width = newWidth 53 | } 54 | 55 | /// Only show the content if the width has been set (is not zero) 56 | if width > 0 { 57 | content(width) 58 | } 59 | } 60 | 61 | public var body: some View { 62 | /// This extra VStack is here because trying to apply frame modifiers to WidthReader may not work correctly without it. 63 | VStack { 64 | if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) { 65 | FittedVStack(alignment: .init(alignment) ?? .center) { 66 | elements 67 | } 68 | } else { 69 | VStack(alignment: alignment, spacing: 0) { 70 | elements 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/FrameUp/Layout/LayoutFromFULayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutFromFULayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A SwiftUI `Layout` that is based on a FrameUp ``FULayout`` 11 | /// 12 | /// `sizeThatFits()` and `placeSubviews()` are generated automatically based on an associated ``FULayout`` 13 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 14 | public protocol LayoutFromFULayout: Layout { 15 | associatedtype AssociatedFULayout: FULayout 16 | 17 | /// The function that generates the associated ``FULayout`` 18 | /// 19 | /// `sizeThatFits()` and `placeSubviews()` are generated automatically based on this ``FULayout`` 20 | /// - Parameter maxSize: The maximum size available for the layout. 21 | /// - Returns: The associated FULayout initialized with the provied max size. 22 | func fuLayout(maxSize: CGSize) -> AssociatedFULayout 23 | } 24 | 25 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 26 | extension LayoutFromFULayout { 27 | /// Generates a dictionary of view sizes keyed by their index from the subview dimensions in the proposed view size. 28 | /// - Parameters: 29 | /// - subviews: A collection of proxies for the subviews of a layout view. 30 | /// - proposal: A proposal for the size of a view. 31 | /// - Returns: A dictionary of view sizes keyed by their index from the subview dimensions in the proposed view size. 32 | public func sizes(for subviews: Subviews, proposal: ProposedViewSize) -> [Int: CGSize] { 33 | subviews 34 | .map { 35 | let dims = $0.dimensions(in: proposal) 36 | return CGSize(width: dims.width, height: dims.height) 37 | } 38 | .enumerated() 39 | .reduce(into: [Int: CGSize]()) { partialResult, indexedSubview in 40 | partialResult[indexedSubview.offset] = indexedSubview.element 41 | } 42 | } 43 | 44 | public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { 45 | let sizes = sizes(for: subviews, proposal: proposal) 46 | let fuLayout = fuLayout(maxSize: proposal.replacingUnspecifiedDimensions()) 47 | return fuLayout.rect(contentOffsets: fuLayout.contentOffsets(sizes: sizes), sizes: sizes).size 48 | } 49 | 50 | public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { 51 | let sizes = sizes(for: subviews, proposal: proposal) 52 | let fuLayout = fuLayout(maxSize: bounds.size) 53 | let offsets = fuLayout.contentOffsets(sizes: sizes) 54 | for (index, subview) in subviews.enumerated() { 55 | if let offset = offsets[index] { 56 | let globalOffset = CGPoint(x: offset.x + bounds.origin.x, y: offset.y + bounds.origin.y) 57 | subview.place(at: globalOffset, proposal: proposal) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/FrameUp/Layout/Layouts/FittedHStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FittedHStack.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2024-05-30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A `Layout` that arranges views like an `HStack` but it ensures the overall height is never larger than the proposed height. This is only used inside ``HeightReader``. 12 | 13 | Example: 14 | ```swift 15 | FittedHStack { 16 | ForEach(["Hello", "World", "More Text"], id: \.self) { item in 17 | Text(item.value) 18 | } 19 | } 20 | ``` 21 | */ 22 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) 23 | struct FittedHStack: Layout, Sendable { 24 | let alignment: FUVerticalAlignment 25 | let spacing: CGFloat 26 | 27 | init(alignment: FUVerticalAlignment = .center, spacing: CGFloat = 0) { 28 | self.alignment = alignment 29 | self.spacing = spacing 30 | } 31 | 32 | func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { 33 | let layout = AnyLayout(HStackLayout(alignment: alignment.alignment, spacing: spacing)) 34 | var cache = layout.makeCache(subviews: subviews) 35 | 36 | let layoutSize = layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache) 37 | 38 | /// The layout height will match the HStack layout size but the height will not exceed the proposal 39 | let height = min( 40 | layoutSize.height, 41 | proposal.replacingUnspecifiedDimensions(by: layoutSize).height 42 | ) 43 | 44 | return .init(width: layoutSize.width, height: height) 45 | } 46 | 47 | func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { 48 | let layout = AnyLayout(HStackLayout(alignment: alignment.alignment, spacing: spacing)) 49 | var cache = layout.makeCache(subviews: subviews) 50 | 51 | let layoutSize = layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache) 52 | 53 | /// Adjust the origin based on the alignment. 54 | let alignmentAdjustment: CGFloat = switch alignment { 55 | case .top, .justified: 0 56 | case .center: 0.5 57 | case .bottom: 1 58 | } 59 | 60 | let originY = bounds.origin.y + ((bounds.height - layoutSize.height) * alignmentAdjustment) 61 | let origin = CGPoint(x: bounds.origin.x, y: originY) 62 | 63 | layout.placeSubviews( 64 | in: .init(origin: origin, size: bounds.size), 65 | proposal: proposal, 66 | subviews: subviews, 67 | cache: &cache 68 | ) 69 | } 70 | } 71 | 72 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) 73 | #Preview { 74 | // ScrollView { 75 | FittedHStack(alignment: .center) { 76 | GeometryReader { proxy in 77 | Color.red.overlay(Text("\(proxy.size.width)")) 78 | 79 | } 80 | 81 | Color.red 82 | .frame(height: 400) 83 | } 84 | .background(Color.blue) 85 | .padding() 86 | // } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/FrameUp/Layout/Layouts/FittedVStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FittedVStack.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2024-05-22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A `Layout` that arranges views like a `VStack` but it ensures the overall width is never larger than the proposed width. This is only used inside ``WidthReader``. 12 | 13 | Example: 14 | ```swift 15 | FittedVStack { 16 | ForEach(["Hello", "World", "More Text"], id: \.self) { item in 17 | Text(item.value) 18 | } 19 | } 20 | ``` 21 | */ 22 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) 23 | struct FittedVStack: Layout, Sendable { 24 | let alignment: FUHorizontalAlignment 25 | let spacing: CGFloat 26 | 27 | init(alignment: FUHorizontalAlignment = .center, spacing: CGFloat = 0) { 28 | self.alignment = alignment 29 | self.spacing = spacing 30 | } 31 | 32 | func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { 33 | let layout = AnyLayout(VStackLayout(alignment: alignment.alignment, spacing: spacing)) 34 | var cache = layout.makeCache(subviews: subviews) 35 | 36 | let layoutSize = layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache) 37 | 38 | /// The layout width will match the VStack layout size but the width will not exceed the proposal 39 | let width = min( 40 | layoutSize.width, 41 | proposal.replacingUnspecifiedDimensions(by: layoutSize).width 42 | ) 43 | 44 | return .init(width: width, height: layoutSize.height) 45 | } 46 | 47 | func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { 48 | let layout = AnyLayout(VStackLayout(alignment: alignment.alignment, spacing: spacing)) 49 | var cache = layout.makeCache(subviews: subviews) 50 | 51 | let layoutSize = layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache) 52 | 53 | /// Adjust the origin based on the alignment. 54 | let alignmentAdjustment: CGFloat = switch alignment { 55 | case .leading, .justified: 0 56 | case .center: 0.5 57 | case .trailing: 1 58 | } 59 | 60 | let originX = bounds.origin.x + ((bounds.width - layoutSize.width) * alignmentAdjustment) 61 | let origin = CGPoint(x: originX, y: bounds.origin.y) 62 | 63 | layout.placeSubviews( 64 | in: .init(origin: origin, size: bounds.size), 65 | proposal: proposal, 66 | subviews: subviews, 67 | cache: &cache 68 | ) 69 | } 70 | } 71 | 72 | @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) 73 | #Preview { 74 | // ScrollView { 75 | FittedVStack(alignment: .center) { 76 | GeometryReader { proxy in 77 | Color.red.overlay(Text("\(proxy.size.width)")) 78 | 79 | } 80 | 81 | Color.red 82 | .frame(width: 400) 83 | } 84 | .background(Color.blue) 85 | .padding() 86 | // } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/FrameUp/Layout/Layouts/HFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HFlowLayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A `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. 12 | 13 | Each row height will be determined by the tallest view in that row. 14 | 15 | Example: 16 | ```swift 17 | HFlowLayout { 18 | ForEach(["Hello", "World", "More Text"], id: \.self) { item in 19 | Text(item.value) 20 | } 21 | } 22 | ``` 23 | */ 24 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 25 | public struct HFlowLayout: LayoutFromFULayout, Sendable { 26 | public let alignment: FUAlignment 27 | public let horizontalSpacing: CGFloat? 28 | public let verticalSpacing: CGFloat? 29 | 30 | /// Creates a `Layout` that arranges views in horizontal rows flowing from one to the next. 31 | /// - Parameters: 32 | /// - alignment: Used to align views vertically in their rows and align rows horizontally relative to each other. Default is top leading. Vertical justification will act as top alignment. 33 | /// - horizontalSpacing: Minimum horizontal spacing between views in a row. 34 | /// - verticalSpacing: Vertical spacing between rows. 35 | public init( 36 | alignment: FUAlignment = .topLeading, 37 | horizontalSpacing: CGFloat? = nil, 38 | verticalSpacing: CGFloat? = nil 39 | ) { 40 | self.alignment = alignment.replacingVerticalJustification() 41 | self.horizontalSpacing = horizontalSpacing 42 | self.verticalSpacing = verticalSpacing 43 | } 44 | 45 | public func fuLayout(maxSize: CGSize) -> HFlow { 46 | HFlow( 47 | alignment: alignment, 48 | maxWidth: maxSize.width, 49 | horizontalSpacing: horizontalSpacing, 50 | verticalSpacing: verticalSpacing 51 | ) 52 | } 53 | } 54 | 55 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 56 | public extension HFlowLayout { 57 | /// Creates a `Layout` that arranges views in horizontal rows flowing from one to the next. 58 | /// - Parameters: 59 | /// - alignment: Used to align views vertically in their rows and align rows horizontally relative to each other. Default is top leading. Vertical justification will act as top alignment. 60 | /// - spacing: Minimum spacing between views. 61 | init( 62 | alignment: FUAlignment = .topLeading, 63 | spacing: CGFloat 64 | ) { 65 | self.init(alignment: alignment, horizontalSpacing: spacing, verticalSpacing: spacing) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/FrameUp/Layout/Layouts/HMasonryLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HMasonryLayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A `Layout` that arranges views into a set number of rows by adding each view to the shortest row. 12 | 13 | Example: 14 | ```swift 15 | HMasonryLayout(rows: 3) { 16 | ForEach(["Hello", "World", "More Text"], id: \.self) { item in 17 | Text(item.value) 18 | } 19 | } 20 | ``` 21 | */ 22 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 23 | public struct HMasonryLayout: LayoutFromFULayout, Sendable { 24 | public let alignment: FUAlignment 25 | public let rows: Int 26 | public let horizontalSpacing: CGFloat? 27 | public let verticalSpacing: CGFloat? 28 | 29 | /// Creates a `Layout` that arranges views into a set number of rows by adding each view to the shortest row. 30 | /// - Parameters: 31 | /// - alignment: Used to align rows horizontally relative to each other. Default is leading. 32 | /// - rows: Number of rows to place views in. 33 | /// - horizontalSpacing: Minimum horizontal spacing between columns. 34 | /// - verticalSpacing: Vertical spacing between views in a column 35 | public init( 36 | alignment: FUAlignment = .leading, 37 | rows: Int, 38 | horizontalSpacing: CGFloat? = nil, 39 | verticalSpacing: CGFloat? = nil 40 | ) { 41 | self.alignment = alignment.replacingVerticalJustification() 42 | self.rows = max(1, rows) 43 | self.horizontalSpacing = horizontalSpacing 44 | self.verticalSpacing = verticalSpacing 45 | } 46 | 47 | public func fuLayout(maxSize: CGSize) -> HMasonry { 48 | HMasonry( 49 | alignment: alignment, 50 | rows: rows, 51 | maxHeight: maxSize.height, 52 | horizontalSpacing: horizontalSpacing, 53 | verticalSpacing: verticalSpacing 54 | ) 55 | } 56 | } 57 | 58 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 59 | public extension HMasonryLayout { 60 | /// Creates a `Layout` that arranges views into a set number of rows by adding each view to the shortest row. 61 | /// - Parameters: 62 | /// - alignment: Used to align rows horizontally relative to each other. Default is leading. 63 | /// - rows: Number of rows to place views in. 64 | /// - spacing: Minimum spacing between views. 65 | init( 66 | alignment: FUAlignment = .leading, 67 | rows: Int, 68 | spacing: CGFloat 69 | ) { 70 | self.init(alignment: alignment, rows: rows, horizontalSpacing: spacing, verticalSpacing: spacing) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/FrameUp/Layout/Layouts/LayoutThatFits.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutThatFits.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-06-09. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A `Layout` that uses the first layout that fits (in the axes provided) from the array of layout preferences. 12 | 13 | Example: 14 | ```swift 15 | LayoutThatFits(in: .horizontal, [HStackLayout(), VStackLayout()]) { 16 | Color.green.frame(width: 50, height: 50) 17 | Color.yellow.frame(width: 50, height: 200) 18 | Color.blue.frame(width: 50, height: 100) 19 | } 20 | ``` 21 | */ 22 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 23 | public struct LayoutThatFits: Layout { 24 | public let axes: Axis.Set 25 | public let layoutPreferences: [AnyLayout] 26 | 27 | /// Creates a `Layout` that uses the first layout that fits (in the axes provided) from the array of layout preferences. 28 | /// - Parameters: 29 | /// - axes: Axes this content must fit in. 30 | /// - layoutPreferences: Layout preferences from largest to smallest. 31 | public init(in axes: Axis.Set = [.horizontal, .vertical], _ layoutPreferences: [any Layout]) { 32 | self.axes = axes 33 | self.layoutPreferences = layoutPreferences.map { AnyLayout($0) } 34 | } 35 | 36 | public func layoutThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> AnyLayout? { 37 | layoutPreferences.first { layout in 38 | var cache = layout.makeCache(subviews: subviews) 39 | let size = layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache) 40 | 41 | let widthFits = size.width <= (proposal.width ?? .infinity) 42 | let heightFits = size.height <= (proposal.height ?? .infinity) 43 | 44 | return (widthFits || !axes.contains(.horizontal)) && (heightFits || !axes.contains(.vertical)) 45 | } 46 | } 47 | 48 | public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { 49 | guard let layout = layoutThatFits(proposal: proposal, subviews: subviews, cache: &cache) ?? layoutPreferences.last else { return CGSize(width: 10, height: 10) } 50 | var cache = layout.makeCache(subviews: subviews) 51 | return layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache) 52 | } 53 | 54 | public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { 55 | guard let layout = layoutThatFits(proposal: proposal, subviews: subviews, cache: &cache) ?? layoutPreferences.last else { return } 56 | var cache = layout.makeCache(subviews: subviews) 57 | layout.placeSubviews(in: bounds, proposal: proposal, subviews: subviews, cache: &cache) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/FrameUp/Layout/Layouts/VFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VFlowLayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A `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. 12 | 13 | Each column width will be determined by the widest view in that column. 14 | 15 | Example: 16 | ```swift 17 | VFlowLayout { 18 | ForEach(["Hello", "World", "More Text"], id: \.self) { item in 19 | Text(item.value) 20 | } 21 | } 22 | ``` 23 | */ 24 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 25 | public struct VFlowLayout: LayoutFromFULayout, Sendable { 26 | public let alignment: FUAlignment 27 | public let horizontalSpacing: CGFloat? 28 | public let verticalSpacing: CGFloat? 29 | 30 | /// Creates a `Layout` that arranges views in vertical columns flowing from one to the next 31 | /// - Parameters: 32 | /// - alignment: Used to align views horizontally in their columns and align columns vertically relative to each other. Default is top leading. 33 | /// - horizontalSpacing: Minimum horizontal spacing between columns. 34 | /// - verticalSpacing: Vertical spacing between views in a column 35 | public init( 36 | alignment: FUAlignment = .topLeading, 37 | horizontalSpacing: CGFloat? = nil, 38 | verticalSpacing: CGFloat? = nil 39 | ) { 40 | self.alignment = alignment.replacingHorizontalJustification() 41 | self.horizontalSpacing = horizontalSpacing 42 | self.verticalSpacing = verticalSpacing 43 | } 44 | 45 | public func fuLayout(maxSize: CGSize) -> VFlow { 46 | VFlow( 47 | alignment: alignment, 48 | maxHeight: maxSize.height, 49 | horizontalSpacing: horizontalSpacing, 50 | verticalSpacing: verticalSpacing 51 | ) 52 | } 53 | } 54 | 55 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 56 | public extension VFlowLayout { 57 | /// Creates a `Layout` that arranges views in vertical columns flowing from one to the next 58 | /// - Parameters: 59 | /// - alignment: Used to align views horizontally in their columns and align columns vertically relative to each other. Default is top leading. 60 | /// - spacing: Minimum spacing between views. 61 | init( 62 | alignment: FUAlignment = .topLeading, 63 | spacing: CGFloat 64 | ) { 65 | self.init(alignment: alignment, horizontalSpacing: spacing, verticalSpacing: spacing) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/FrameUp/Layout/Layouts/VMasonryLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMasonryLayout.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A `Layout` that arranges views into a set number of columns by adding each view to the shortest column. 12 | 13 | Example: 14 | ```swift 15 | VMasonryLayout(columns: 3) { 16 | ForEach(["Hello", "World", "More Text"], id: \.self) { item in 17 | Text(item.value) 18 | } 19 | } 20 | ``` 21 | */ 22 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 23 | public struct VMasonryLayout: LayoutFromFULayout, Sendable { 24 | public let alignment: FUAlignment 25 | public let columns: Int 26 | public let horizontalSpacing: CGFloat? 27 | public let verticalSpacing: CGFloat? 28 | 29 | /// Creates a `Layout` that arranges views into a set number of columns by adding each view to the shortest column. 30 | /// - Parameters: 31 | /// - alignment: Used to align columns vertically relative to each other. Default is top. 32 | /// - columns: Number of columns to place views in. 33 | /// - horizontalSpacing: Minimum horizontal spacing between columns. 34 | /// - verticalSpacing: Vertical spacing between views in a column 35 | public init( 36 | alignment: FUAlignment = .top, 37 | columns: Int, 38 | horizontalSpacing: CGFloat? = nil, 39 | verticalSpacing: CGFloat? = nil 40 | ) { 41 | self.alignment = alignment.replacingHorizontalJustification() 42 | self.columns = max(1, columns) 43 | self.horizontalSpacing = horizontalSpacing 44 | self.verticalSpacing = verticalSpacing 45 | } 46 | 47 | public func fuLayout(maxSize: CGSize) -> VMasonry { 48 | VMasonry( 49 | alignment: alignment, 50 | columns: columns, 51 | maxWidth: maxSize.width, 52 | horizontalSpacing: horizontalSpacing, 53 | verticalSpacing: verticalSpacing 54 | ) 55 | } 56 | } 57 | 58 | @available(iOS 16, macOS 13, watchOS 9, tvOS 16, *) 59 | public extension VMasonryLayout { 60 | /// Creates a `Layout` that arranges views into a set number of columns by adding each view to the shortest column. 61 | /// - Parameters: 62 | /// - alignment: Used to align columns vertically relative to each other. Default is top. 63 | /// - columns: Number of columns to place views in. 64 | /// - spacing: Minimum spacing between views. 65 | init( 66 | alignment: FUAlignment = .top, 67 | columns: Int, 68 | spacing: CGFloat 69 | ) { 70 | self.init(alignment: alignment, columns: columns, horizontalSpacing: spacing, verticalSpacing: spacing) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/FrameUp/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/FrameUp/TabMenu/NamedAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NamedAction.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-09-11. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if os(iOS) 11 | public struct NamedAction { 12 | public let name: Text 13 | public let action: () -> Void 14 | } 15 | 16 | public extension NamedAction { 17 | init(_ name: LocalizedStringKey, action: @escaping () -> Void) { 18 | self.name = Text(name) 19 | self.action = action 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/FrameUp/TabMenu/TabMenuItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabMenuItem.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if os(iOS) 11 | /// A tab menu item that can be used in TabMenu. 12 | public struct TabMenuItem: Equatable { 13 | /// Icon silhouette for the tab menu item. 14 | public let icon: AnyView 15 | /// Label used on the tab menu (nil for blank). 16 | public let name: String? 17 | /// Hashable tab id. 18 | public let tab: Tab 19 | 20 | /// Creates a TabMenuItem with any view as the icon, an optional label and a hashable tab id. 21 | /// - Parameters: 22 | /// - icon: View used as the icon silhouette for the tab menu item. 23 | /// - name: Label used on the tab menu (nil for blank). 24 | /// - tab: Hashable tab id 25 | public init(icon: AnyView, name: String? = nil, tab: Tab) { 26 | self.icon = icon 27 | self.name = name 28 | self.tab = tab 29 | } 30 | 31 | /// Creates a TabMenuItem with an image for the icon, an optional label and a hashable tab id. 32 | /// - Parameters: 33 | /// - image: Image used as the icon silhouette for the tab menu item. 34 | /// - name: Label used on the tab menu (nil for blank). 35 | /// - tab: Hashable tab id. 36 | public init(image: Image, name: String? = nil, tab: Tab) { 37 | self.icon = AnyView(image.resizable()) 38 | self.name = name 39 | self.tab = tab 40 | } 41 | 42 | public static func == (lhs: TabMenuItem, rhs: TabMenuItem) -> Bool { 43 | lhs.tab == rhs.tab 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/FrameUp/TwoSidedView/BackfaceCull.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackfaceCull.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-09-06. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A shape that draws a rectangle matching the frame when the rotation angle is facing forward (angles between -90 and 90 degrees) and nothing when facing backwards (angles between 90 and 270 degrees). 11 | struct BackfaceCull: Shape { 12 | /// Degrees of rotation. Any additional 360 degree rotaitons will be removed before evaluating. 13 | var degrees: CGFloat 14 | 15 | var animatableData: CGFloat { 16 | get { degrees } 17 | set { degrees = newValue } 18 | } 19 | 20 | func path(in rect: CGRect) -> Path { 21 | var path = Path() 22 | switch abs(degrees).truncatingRemainder(dividingBy: 360) { 23 | case 90...270: break 24 | default: path.addRect(rect) 25 | } 26 | return path 27 | } 28 | } 29 | 30 | #Preview { 31 | VStack { 32 | BackfaceCull(degrees: .zero) 33 | .fill(.blue) 34 | 35 | BackfaceCull(degrees: 180) 36 | .fill(.red) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/FrameUp/Widget/AccessoryInlineImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessoryInlineImage.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-09-05. 6 | // 7 | 8 | #if os(iOS) 9 | import SwiftUI 10 | 11 | /// An image that will be scaled and have the rendering mode adjusted to work inside an `accessoryInline` widget. 12 | /// 13 | /// Use inside a Label's icon property. 14 | /// 15 | /// ```swift 16 | /// Label { 17 | /// Text("Label Text") 18 | /// } icon: { 19 | /// AccessoryInlineImage("myImage") 20 | /// } 21 | /// ``` 22 | /// 23 | @available(iOSApplicationExtension 16.0, *) 24 | public struct AccessoryInlineImage: View { 25 | public let uiImage: UIImage 26 | 27 | /// Creates an image that will be scaled and have the rendering mode adjusted to work inside an `accessoryInline` widget. 28 | /// - Parameter uiImage: Base image to use. 29 | public init?(_ uiImage: UIImage) { 30 | if uiImage.isSymbolImage { 31 | self.uiImage = uiImage.withRenderingMode(.alwaysTemplate) 32 | } else { 33 | let widgetSize = WidgetSize.accessoryInline 34 | let imageSize = widgetSize.sizeForCurrentDevice(iPadTarget: .designCanvas) ?? widgetSize.minimumSize 35 | 36 | guard let uiImage = uiImage 37 | .scaledToFit(imageSize)? 38 | .withRenderingMode(.alwaysTemplate) 39 | else { return nil } 40 | self.uiImage = uiImage 41 | } 42 | } 43 | 44 | public var body: Image { 45 | Image(uiImage: uiImage) 46 | } 47 | } 48 | 49 | public extension AccessoryInlineImage { 50 | /// Creates an image that will be scaled and have the rendering mode adjusted to work inside an `accessoryInline` widget. 51 | /// - Parameter name: The name of the image asset or file. 52 | /// - Parameter bundle: The bundle containing the image file or asset catalog. Specify nil to search the app’s main bundle. 53 | init?(_ name: String, bundle: Bundle? = nil) { 54 | guard let uiImage = UIImage(named: name, in: bundle, with: nil) else { return nil } 55 | self.init(uiImage) 56 | } 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /Sources/FrameUp/Widget/WidgetFamily+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetFamily+extensions.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-05-28. 6 | // 7 | 8 | #if canImport(WidgetKit) 9 | import SwiftUI 10 | import WidgetKit 11 | 12 | @available(watchOS 9, *) 13 | public extension WidgetFamily { 14 | #if os(iOS) 15 | /// Supported families for the current device. 16 | @preconcurrency @MainActor 17 | static var supportedFamiliesForCurrentDevice: [WidgetFamily] { 18 | WidgetSize.supportedSizesForCurrentDevice.compactMap { $0.widgetFamily } 19 | } 20 | #endif 21 | 22 | /// Equivalent widget size. Only returns nil for unknown values. 23 | var size: WidgetSize? { 24 | switch self { 25 | case .systemSmall: return .small 26 | case .systemMedium: return .medium 27 | case .systemLarge: return .large 28 | case .systemExtraLarge: return .extraLarge 29 | case .accessoryCircular: return .accessoryCircular 30 | case .accessoryRectangular: return .accessoryRectangular 31 | case .accessoryInline: return .accessoryInline 32 | case .accessoryCorner: return nil 33 | @unknown default: return nil 34 | } 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Sources/FrameUp/Widget/WidgetSize+CurrentDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetSize+CurrentDevice.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-01-25. 6 | // 7 | 8 | 9 | #if os(iOS) 10 | import SwiftUI 11 | 12 | public extension WidgetSize { 13 | /// The screen size ignoring orientation. 14 | @preconcurrency @MainActor 15 | private static let currentScreenSize = 16 | UIScreen.main.fixedCoordinateSpace.bounds.size 17 | 18 | /// The current device. 19 | @preconcurrency @MainActor 20 | private static let currentDevice = UIDevice.current.userInterfaceIdiom 21 | 22 | /// Find the supported sizes for a specified device 23 | /// - Parameter device: iPhone, iPad, etc 24 | /// - Returns: An array of widget sizes 25 | static func supportedSizes(for device: UIUserInterfaceIdiom) -> [WidgetSize] { 26 | switch device { 27 | case .pad: 28 | if #available(iOS 15.0, *) { 29 | return [.small, .medium, .large, .extraLarge] 30 | } else { 31 | return [.small, .medium, .large] 32 | } 33 | case .phone: 34 | if #available(iOS 16, *) { 35 | return [.small, .medium, .large, .accessoryCircular, .accessoryRectangular, .accessoryInline] 36 | } else { 37 | return [.small, .medium, .large] 38 | } 39 | default: 40 | return [] 41 | } 42 | } 43 | 44 | /// Supported widget sizes for the current device. 45 | @preconcurrency @MainActor 46 | static var supportedSizesForCurrentDevice: [WidgetSize] { 47 | supportedSizes(for: currentDevice) 48 | } 49 | 50 | /// Size for this widget on the current device. 51 | /// - Parameter iPadTarget: Widget frame target. iPad widgets have a design canvas frame used for laying out the content, and a smaller Home Screen frame that the content is scaled to fit. 52 | /// - Returns: Size for this widget for the current device. Zero if device does not have widgets or if no size is available. 53 | @preconcurrency @MainActor 54 | func sizeForCurrentDevice(iPadTarget: WidgetTarget) -> CGSize? { 55 | switch Self.currentDevice { 56 | case .pad: 57 | return sizeForiPad(screenSize: Self.currentScreenSize, target: iPadTarget) 58 | case .phone: 59 | return sizeForiPhone(screenSize: Self.currentScreenSize) 60 | default: 61 | return nil 62 | } 63 | } 64 | 65 | /// How much the widget is scaled down to fit on the Home Screen. 66 | /// 67 | /// Home Screen width divided by design canvas width. iPhone value will always be 1. 68 | @preconcurrency @MainActor 69 | var scaleFactorForCurrentDevice: CGFloat? { 70 | switch Self.currentDevice { 71 | case .pad: 72 | return scaleFactorForiPad(screenSize: Self.currentScreenSize) 73 | case .phone: 74 | return 1 75 | default: 76 | return nil 77 | } 78 | } 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /Sources/FrameUp/Widget/WidgetSize+WidgetKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetSize+WidgetKit.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-17. 6 | // 7 | 8 | #if canImport(WidgetKit) 9 | import Foundation 10 | import WidgetKit 11 | 12 | @available(watchOS 9, *) 13 | public extension WidgetSize { 14 | /// Equivalent widget family. Optional as extraLarge will return nil unless running iOS 15.0 or later. 15 | var widgetFamily: WidgetFamily? { 16 | switch self { 17 | case .small: 18 | #if os(watchOS) 19 | return nil 20 | #else 21 | return .systemSmall 22 | #endif 23 | case .medium: 24 | #if os(watchOS) 25 | return nil 26 | #else 27 | return .systemMedium 28 | #endif 29 | case .large: 30 | #if os(watchOS) 31 | return nil 32 | #else 33 | return .systemLarge 34 | #endif 35 | case .extraLarge: 36 | if #available(iOS 15.0, *) { 37 | #if os(iOS) 38 | return .systemExtraLarge 39 | #else 40 | return nil 41 | #endif 42 | } else { 43 | return nil 44 | } 45 | case .accessoryRectangular: 46 | if #available(iOS 16.0, *) { 47 | #if os(iOS) 48 | return .accessoryRectangular 49 | #else 50 | return nil 51 | #endif 52 | } else { 53 | return nil 54 | } 55 | case .accessoryCircular: 56 | if #available(iOS 16.0, *) { 57 | #if os(iOS) 58 | return .accessoryCircular 59 | #else 60 | return nil 61 | #endif 62 | } else { 63 | return nil 64 | } 65 | case .accessoryInline: 66 | if #available(iOS 16.0, *) { 67 | #if os(iOS) 68 | return .accessoryInline 69 | #else 70 | return nil 71 | #endif 72 | } else { 73 | return nil 74 | } 75 | } 76 | } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Deprecated/Flow/LegacyFlowContentSizeKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegacyFlowContentSizeKey.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-06-11. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Preference key used to pass child view sizes up the hierarchy. 11 | /// 12 | /// Used by ``HFlowLegacy`` and ``VFlowLegacy``. 13 | @available(*, deprecated, renamed: "FULayoutSizeKey") 14 | public struct FlowContentSizeKey: PreferenceKey { 15 | public typealias Value = [Int: CGSize] 16 | public static let defaultValue: [Int: CGSize] = [:] 17 | public static func reduce(value: inout Value, nextValue: () -> Value) { 18 | nextValue().forEach { 19 | value.updateValue($0.value, forKey: $0.key) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Internal/CG+equals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CG+equals.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-09-13. 6 | // 7 | 8 | import Foundation 9 | 10 | internal extension CGRect { 11 | func equals(_ other: Self, precision: CGFloat) -> Bool { 12 | origin.equals(other.origin, precision: precision) && size.equals(other.size, precision: precision) 13 | } 14 | } 15 | 16 | internal extension CGPoint { 17 | func equals(_ other: Self, precision: CGFloat) -> Bool { 18 | x.equals(other.x, precision: precision) && y.equals(other.y, precision: precision) 19 | } 20 | } 21 | 22 | internal extension CGSize { 23 | func equals(_ other: Self, precision: CGFloat) -> Bool { 24 | width.equals(other.width, precision: precision) && height.equals(other.height, precision: precision) 25 | } 26 | } 27 | 28 | internal extension CGFloat { 29 | func equals(_ other: CGFloat, precision: CGFloat) -> Bool { 30 | Int((self / precision).rounded()) == Int((other / precision).rounded()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Internal/CGSize+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+extensions.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An internal alternative type to CGSize 11 | /// 12 | /// Used to add Proportionable conformance to CGSize internally only. 13 | fileprivate struct ProportionableSize: Proportionable { 14 | var width: CGFloat 15 | var height: CGFloat 16 | 17 | var size: CGSize { 18 | CGSize(width: width, height: height) 19 | } 20 | } 21 | 22 | internal extension CGSize { 23 | fileprivate var proportionableSize: ProportionableSize { 24 | ProportionableSize(width: width, height: height) 25 | } 26 | 27 | var aspectFormat: AspectFormat { 28 | proportionableSize.aspectFormat 29 | } 30 | 31 | var aspectRatio: CGFloat { 32 | proportionableSize.aspectRatio 33 | } 34 | 35 | var swappingWidthAndHeight: CGSize { 36 | proportionableSize.swappingWidthAndHeight.size 37 | } 38 | 39 | var minDimension: CGFloat { 40 | proportionableSize.minDimension 41 | } 42 | 43 | var maxDimension: CGFloat { 44 | proportionableSize.maxDimension 45 | } 46 | 47 | init(width: CGFloat, aspectRatio: CGFloat) { 48 | self = ProportionableSize(width: width, aspectRatio: aspectRatio).size 49 | } 50 | 51 | init(height: CGFloat, aspectRatio: CGFloat) { 52 | self = ProportionableSize(height: height, aspectRatio: aspectRatio).size 53 | } 54 | 55 | static func square(_ width: CGFloat) -> Self { 56 | ProportionableSize.square(width).size 57 | } 58 | 59 | func scaledToFit(_ frame: CGSize) -> CGSize { 60 | proportionableSize.scaledToFit(frame.proportionableSize).size 61 | } 62 | 63 | func scaledToFill(_ frame: CGSize) -> CGSize { 64 | proportionableSize.scaledToFill(frame.proportionableSize).size 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Internal/Dictionary+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+extensions.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-09-09. 6 | // 7 | 8 | import Foundation 9 | 10 | internal extension Dictionary { 11 | /// Adds a new key/value pair or replaces an existing element with the same key. 12 | /// - Parameter element: Key-value pair to add. 13 | mutating func update(with element: (key: Key, value: Value)) { 14 | updateValue(element.value, forKey: element.key) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Internal/View+IfAvailable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+IfAvailable.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-07-14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | /// Applies the given transform or returns the untransformed view. 12 | /// 13 | /// Useful for availability branching on view modifiers. Do not branch with any properties that may change during runtime as this will cause errors. 14 | /// - Parameters: 15 | /// - transform: The transform to apply to the source `View`. 16 | /// - Returns: The view transformed by the transform. 17 | @ViewBuilder 18 | func ifAvailable(@ViewBuilder _ transform: (Self) -> (some View)?) -> some View { 19 | if let transformed = transform(self) { 20 | transformed 21 | } else { 22 | self 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Internal/View+onPreferenceChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+onPreferenceChange.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2024-11-28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | internal extension View { 11 | #if compiler(>=6.0) && compiler(<6.1) 12 | /// Adds an action to perform when the specified preference key's value 13 | /// changes. This action is wrapped in a MainActor Task for Swift compiler 6.0 to 6.0.3 due to the Sendable requirement for those versions. 14 | /// 15 | /// - Parameters: 16 | /// - key: The key to monitor for value changes. 17 | /// - action: The action to perform when the value for `key` changes. The 18 | /// `action` closure passes the new value as its parameter. 19 | /// 20 | /// - Returns: A view that triggers `action` when the value for `key` 21 | /// changes. 22 | @preconcurrency @inlinable nonisolated func onPreferenceChangeMainActor(_ key: K.Type = K.self, perform action: @escaping @MainActor (K.Value) -> Void) -> some View where K : PreferenceKey, K.Value : Equatable & Sendable { 23 | onPreferenceChange(key) { newValue in 24 | Task { @MainActor in 25 | action(newValue) 26 | } 27 | } 28 | } 29 | #else 30 | /// Adds an action to perform when the specified preference key's value 31 | /// changes. This action is wrapped in a MainActor Task for Swift compiler 6.0 to 6.0.3 due to the Sendable requirement for those versions. 32 | /// 33 | /// - Parameters: 34 | /// - key: The key to monitor for value changes. 35 | /// - action: The action to perform when the value for `key` changes. The 36 | /// `action` closure passes the new value as its parameter. 37 | /// 38 | /// - Returns: A view that triggers `action` when the value for `key` 39 | /// changes. 40 | @inlinable nonisolated func onPreferenceChangeMainActor(_ key: K.Type = K.self, perform action: @escaping (K.Value) -> Void) -> some View where K : PreferenceKey, K.Value : Equatable { 41 | onPreferenceChange(key, perform: action) 42 | } 43 | #endif 44 | } 45 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Public/Axis.Set+Hashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Axis.Set+Hashable.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2022-10-19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if compiler(<6) 11 | extension Axis.Set: Hashable { } 12 | #else 13 | extension Axis.Set: @retroactive Hashable { } 14 | #endif 15 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Public/Dictionary+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+publicExtensions.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-10. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Dictionary where Dictionary.Key: Comparable { 11 | /// Creates an array of key value pairs sorted by key. 12 | func sortedByKey() -> [(key: Key, value: Value)] { 13 | sorted { $0.key < $1.key } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Public/UIImage+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+publicExtensions.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2023-09-02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if os(iOS) 11 | public extension UIImage { 12 | func scale(_ scale: CGFloat) -> UIImage? { 13 | let newSize = CGSize(width: size.width * scale, height: size.height * scale) 14 | /// Change this to UIGraphicsImageRenderer(size: newSize) 15 | UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale) 16 | draw(in: .init(origin: .zero, size: newSize), blendMode: .normal, alpha: 1) 17 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 18 | UIGraphicsEndImageContext() 19 | return newImage 20 | } 21 | 22 | func scaledToFit(_ frame: CGSize) -> UIImage? { 23 | let newSize = size.scaledToFit(frame) 24 | UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale) 25 | draw(in: .init(origin: .zero, size: newSize), blendMode: .normal, alpha: 1) 26 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 27 | UIGraphicsEndImageContext() 28 | return newImage 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/FrameUp/_Extensions-Public/View+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+extensions.swift 3 | // FrameUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | /// Positions this view within an invisible frame with the specified size. 12 | /// - Parameters: 13 | /// - size: The fixed size of the resulting view. 14 | /// - alignment: The alignment of this view inside the resulting view. alignment applies if this view is smaller than the size given by the resulting frame. 15 | /// - Returns: A view with fixed size unless a nil size is provided. 16 | func frame(_ size: CGSize?, alignment: Alignment = .center) -> some View { 17 | self.frame(width: size?.width, height: size?.height, alignment: alignment) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/FrameUpTests/FrameUpTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FrameUp 3 | 4 | final class FrameUpTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | } 11 | --------------------------------------------------------------------------------