├── .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 |
--------------------------------------------------------------------------------