-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 388d368
Showing
20 changed files
with
942 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/configuration/registries.json | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata | ||
.netrc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// swift-tools-version: 6.0 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "AttachmentsEverywhere", | ||
platforms: [.macOS(.v15), .iOS(.v18)], | ||
products: [ | ||
.library( | ||
name: "AttachmentsEverywhere", | ||
targets: ["AttachmentsEverywhere"]), | ||
], | ||
targets: [ | ||
.target( | ||
name: "AttachmentsEverywhere", | ||
resources: [.process("Resources/GlassMaterial.usda")] | ||
), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# AttachmentsEverywhere | ||
Backports `RealityView`'s `init(make:update:attachments)` to non-visionOS platforms. | ||
|
||
## Basic Usage | ||
- Use as a standard SPM dependency | ||
- Import `AttachmentsEverywhere` in your code | ||
- Replace any `RealityView` that uses `init(make:update:attachments:)` with `AttachableRealityView` | ||
- Your code will probably (hopefully_ compile, though it may not function properly without tweaking | ||
|
||
## Advanced Usage | ||
In order to approach the usability of attachments on visionOS, `AttachmentsEverywhere` provides some | ||
extensions on top of the `RealityKit` API. These extensions are not available on visionOS, and | ||
therefore must be wrapped inside `#if !os(visionOS)`. | ||
|
||
### Glass Background Customization | ||
By default, `AttachmentsEverywhere` adds a glass background (rather, a crude facsimile of it) to | ||
attachments. Use the `.glassBackgroundStyle(_:)` modifier on `Attachment` with a | ||
`GlassBackgroundType` in order to customize this behavior: | ||
- `.capsule`: The view's glass background will be a capsule | ||
(corner radius equal to half the height of the attachment). | ||
- `.roundedRectangle(cornerRadius:)`: The view's glass background will be a rounded rectangle, | ||
with a corner radius defined in points. | ||
- `.none`: The view will not have a glass background. | ||
|
||
### Gestures | ||
Use `.onTapGesture(_:)` on `Attachment` in order to respond to tap gestures. At this time, this is | ||
the only supported gesture. Your closure will be passed a `CGPoint` which is the location of the tap | ||
in your view's coordinate space. | ||
|
||
### Attachment Scale | ||
By default, attachment view entities are scaled such that 300 points is equivalent to one | ||
(RealityView) meter. In order to customize this scaling, apply the | ||
`.attachmentScale(pointsPerMeter:)` view modifier outside your `AttachableRealityView`. | ||
|
||
## Important Considerations / Quirks | ||
`AttachmentsEverywhere` relies on `SwiftUI`'s current behavior regarding `RealityView` view updates. | ||
This behavior could change in future OS releases (i.e., past iOS 18.0 and macOS 15.0) and some | ||
features of this library may cease functioning. This library is intended as a stopgap solution until | ||
Apple (hopefully) adds attachments to non-visionOS platforms. As such, this library has some quirks | ||
regarding its functionality that should be taken into account. | ||
|
||
### Automatic Re-Rendering of Attachment Views | ||
`Attachment`s will be re-rendered when `ImageRenderer` determines their state to have changed. | ||
Crucially, this only seems to take into account `State`s, `Binding`s, etc. held *inside* the | ||
attachment views. State held outside of `AttachableRealityView` *will not* automatically re-render | ||
attachment views. Such state *must* be passed into `Attachment`s with `Binding` (and similar). | ||
|
||
### Automatic Insertion / Deletion / Modification of Attachments | ||
Every time `RealityView` would call the `update` closure, `AttachmentsEverywhere` reevaluates the | ||
`attachments` closure. This means that `Attachment`s can be dynamically added or removed (with | ||
result builder control flow) in response to state changes, though your surrounding view must depend | ||
on these state variables. Just like with `RealityView`, any attachments must be added to the scene | ||
manually. Any attachments that are removed due to a state change will be automatically removed from | ||
the scene. | ||
|
||
**At this time (until I find a clean way to make it function), `ForEach` is not supported | ||
in the `attachments` closure.** Instead, use a for-in loop, as it is supported. | ||
|
||
## Library To-Do | ||
- [ ] Support `ForEach` (if possible) in order to bolster code compatibility | ||
- [ ] Support other gestures | ||
- [ ] Create better glass material |
172 changes: 172 additions & 0 deletions
172
Sources/AttachmentsEverywhere/API/AttachableRealityView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// | ||
// AttachableRealityView.swift | ||
// AttachmentsEverywhere | ||
// | ||
// Created by Elijah Santos on 8/7/24. | ||
// | ||
|
||
#if !os(visionOS) | ||
|
||
import SwiftUI | ||
import RealityKit | ||
|
||
/// Replicates (to the extent possible) the attachments functionality of `RealityView` on visionOS. | ||
/// | ||
/// Use as you would a `RealityView`. On visionOS, `AttachableRealityView` is a typealias for `RealityView`. | ||
@MainActor | ||
public struct AttachableRealityView<Placeholder: View>: View { | ||
private enum AttachmentUpdateState { | ||
case notInProgress, preUpdate, postUpdate | ||
} | ||
private let make: @MainActor (inout RealityViewCameraContent, RealityViewAttachments) async -> Void | ||
private let update: (@MainActor (inout RealityViewCameraContent, RealityViewAttachments) -> Void)? | ||
private let placeholder: Placeholder? | ||
private let attachments: () -> AttachmentContent | ||
|
||
@Environment(\.attachmentPointsPerMeter) private var pointsPerMeter | ||
@StateObject private var updator = Updator() | ||
@State private var n = 0 | ||
@State private var attachmentUpdateState = AttachmentUpdateState.notInProgress | ||
@State private var cachedAttachments: RealityViewAttachmentsBacking? = nil | ||
|
||
public init( | ||
make: @escaping @MainActor (inout RealityViewCameraContent, RealityViewAttachments) async -> Void, | ||
update: (@MainActor (inout RealityViewCameraContent, RealityViewAttachments) -> Void)? = nil, | ||
@AttachmentContentBuilder attachments: @escaping () -> AttachmentContent | ||
) where Placeholder == Never { | ||
self.make = make | ||
self.update = update | ||
self.placeholder = nil | ||
self.attachments = attachments | ||
} | ||
|
||
public init( | ||
make: @escaping @MainActor (inout RealityViewCameraContent, RealityViewAttachments) async -> Void, | ||
update: (@MainActor (inout RealityViewCameraContent, RealityViewAttachments) -> Void)? = nil, | ||
@ViewBuilder placeholder: () -> Placeholder, | ||
@AttachmentContentBuilder attachments: @escaping () -> AttachmentContent | ||
) { | ||
self.make = make | ||
self.update = update | ||
self.placeholder = placeholder() | ||
self.attachments = attachments | ||
} | ||
|
||
// TODO: reimplement non-attachment initializers, for convenience | ||
|
||
private func createAttachments() async { | ||
self.cachedAttachments = await RealityViewAttachmentsBacking( | ||
metersPerRenderedPx: 1 / (pointsPerMeter * ImageGenerator_renderScale), | ||
views: attachments()) { | ||
Task { @MainActor in | ||
do { | ||
var attachments = cachedAttachments | ||
try await attachments?.regenerateAttachments() | ||
self.cachedAttachments = attachments | ||
self.updator.forceViewUpdate() | ||
} catch { | ||
print("[AttachmentsEverywhere] warning: failed to update attachments: \(error.localizedDescription)") | ||
} | ||
} | ||
} | ||
} | ||
|
||
private func updateNewAndRemovedAttachments( | ||
new: AttachmentContent, | ||
cached: RealityViewAttachmentsBacking, | ||
content: inout RealityViewCameraContent | ||
) -> RealityViewAttachments? { | ||
if attachmentUpdateState == .postUpdate { | ||
// attachments now loaded; continue update with new attachments | ||
DispatchQueue.main.async { | ||
attachmentUpdateState = .notInProgress | ||
} | ||
return cached.collection() | ||
} | ||
let newPairs = new.filter { !cached.keys.contains($0.key) } | ||
let removedKeys = cached.keys.filter { !new.keys.contains($0) } | ||
if !removedKeys.isEmpty { | ||
self.cachedAttachments?.removeAttachments(forKeys: removedKeys, from: &content) | ||
} | ||
|
||
if !newPairs.isEmpty { | ||
Task { @MainActor [newPairs] in | ||
attachmentUpdateState = .preUpdate | ||
var c = self.cachedAttachments | ||
await c?.insertAttachments(newPairs) | ||
self.cachedAttachments = c | ||
updator.forceViewUpdate() | ||
attachmentUpdateState = .postUpdate | ||
} | ||
// defer update until attachments loaded | ||
return nil | ||
} | ||
// no attachments to load; proceed with update | ||
return cached.collection(excluding: removedKeys) | ||
} | ||
|
||
private func doUpdate(_ content: inout RealityViewCameraContent) { | ||
_ = updator.updateCount | ||
guard let cachedAttachments else { return } | ||
let newAttachments = attachments() | ||
if let pushedAttachments = updateNewAndRemovedAttachments( | ||
new: newAttachments, | ||
cached: cachedAttachments, | ||
content: &content | ||
) { | ||
update?(&content, pushedAttachments) | ||
} | ||
} | ||
|
||
public var body: some View { | ||
Group { | ||
if let placeholder { | ||
RealityView { content in | ||
_ = updator.updateCount | ||
await createAttachments() | ||
await make(&content, cachedAttachments!.collection()) | ||
} update: { content in | ||
doUpdate(&content) | ||
} placeholder: { | ||
placeholder | ||
} | ||
} else { | ||
RealityView { content in | ||
_ = updator.updateCount | ||
await createAttachments() | ||
await make(&content, cachedAttachments!.collection()) | ||
} update: { content in | ||
doUpdate(&content) | ||
} | ||
|
||
} | ||
} | ||
.gesture( | ||
SpatialTapGesture() | ||
.targetedToEntity(where: .has(ViewAttachmentComponent.self)) | ||
.onEnded { value in | ||
guard let entity = value.entity as? ViewAttachmentEntity, | ||
let tapPoint3D = value.hitTest( | ||
point: value.location, | ||
in: .local).first?.position | ||
else { | ||
return | ||
} | ||
let attachmentOrientation = entity.orientation(relativeTo: nil) | ||
let attachmentPosition = entity.position(relativeTo: nil) | ||
let xyAlignedPoint = simd_act(simd_negate(attachmentOrientation), tapPoint3D - attachmentPosition) | ||
let resultantPoint = CGPoint( | ||
x: Double(((xyAlignedPoint.x + (entity.attachment.bounds.extents.x / 2)) * pointsPerMeter)) - Attachment_padding, | ||
y: Double((entity.attachment.bounds.extents.y - xyAlignedPoint.y - (entity.attachment.bounds.extents.y / 2)) * pointsPerMeter) - Attachment_padding) | ||
entity.attachment.tapResponder?(resultantPoint) | ||
} | ||
) | ||
.overlay { | ||
// force swiftui to do a view update when updator changes | ||
Text(updator.updateCount.description) | ||
.hidden() | ||
} | ||
} | ||
} | ||
|
||
#endif |
14 changes: 14 additions & 0 deletions
14
Sources/AttachmentsEverywhere/API/AttachableRealityView_visionOS.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// | ||
// AttachableRealityView_visionOS.swift | ||
// AttachmentsEverywhere | ||
// | ||
// Created by Elijah Santos on 8/7/24. | ||
// | ||
|
||
#if os(visionOS) | ||
import SwiftUI | ||
import RealityKit | ||
|
||
// since visionOS has attachments, no custom implementation is needed; only a typealias | ||
public typealias AttachableRealityView = RealityView | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// | ||
// File.swift | ||
// AttachmentsEverywhere | ||
// | ||
// Created by Elijah Santos on 8/11/24. | ||
// | ||
|
||
import Foundation |
43 changes: 43 additions & 0 deletions
43
...AttachmentsEverywhere/API/Attachments/Attachment Customization/Attachment+modifiers.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// | ||
// Attachment+modifiers.swift | ||
// AttachmentsEverywhere | ||
// | ||
// Created by Elijah Santos on 8/11/24. | ||
// | ||
|
||
import Foundation | ||
import SwiftUI | ||
|
||
#if !os(visionOS) | ||
|
||
extension Attachment { | ||
internal func withConfigurationValue<T>( | ||
_ kp: WritableKeyPath<AttachmentCustomization, T>, | ||
setTo newValue: T | ||
) -> Attachment { | ||
var copy = self | ||
copy.configuration[keyPath: kp] = newValue | ||
return copy | ||
} | ||
|
||
public func glassBackgroundStyle(_ style: GlassBackgroundType) -> Self { | ||
self.withConfigurationValue(\.backgroundType, setTo: style) | ||
} | ||
|
||
public func onTapGesture( | ||
_ action: @MainActor @escaping (_ location: CGPoint) -> Void | ||
) -> Self { | ||
let new: @MainActor (CGPoint) -> Void | ||
if let existing = configuration.tapResponder { | ||
new = { | ||
existing($0) | ||
action($0) | ||
} | ||
} else { | ||
new = action | ||
} | ||
return self.withConfigurationValue(\.tapResponder, setTo: new) | ||
} | ||
} | ||
|
||
#endif |
18 changes: 18 additions & 0 deletions
18
...achmentsEverywhere/API/Attachments/Attachment Customization/AttachmentCustomization.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// | ||
// AttachmentCustomization.swift | ||
// AttachmentsEverywhere | ||
// | ||
// Created by Elijah Santos on 8/11/24. | ||
// | ||
|
||
import Foundation | ||
import CoreGraphics | ||
|
||
public struct AttachmentCustomization: Sendable { | ||
var backgroundType: GlassBackgroundType | ||
var tapResponder: (@MainActor (CGPoint) -> Void)? | ||
|
||
static let `default`: AttachmentCustomization = .init( | ||
backgroundType: .roundedRectangle(cornerRadius: 20), | ||
tapResponder: nil) | ||
} |
24 changes: 24 additions & 0 deletions
24
.../AttachmentsEverywhere/API/Attachments/Attachment Customization/GlassBackgroundType.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// | ||
// GlassBackgroundType.swift | ||
// AttachmentsEverywhere | ||
// | ||
// Created by Elijah Santos on 8/11/24. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Describes the type of glass background placed behind an attachment. | ||
/// | ||
/// - SeeAlso: `Attachment.glassBackgroundStyle(_:)` | ||
public enum GlassBackgroundType: Hashable, Sendable { | ||
/// A capsule (flat top and bottom edges, circular left and right). | ||
case capsule | ||
|
||
/// A rounded rectangle. | ||
/// | ||
/// `cornerRadius` is defined in points. | ||
case roundedRectangle(cornerRadius: Double) | ||
|
||
/// No glass background. | ||
case none | ||
} |
Oops, something went wrong.