class Coordinator: NSObject, UIContextMenuInteractionDelegate, ContextMenuManagerDelegate {
var container: ContextMenuContainer
var auxiliaryContent: AuxiliaryContent
var menuItems: () -> [UIMenuElement]
init(container: ContextMenuContainer) {
self.container = container
self.auxiliaryContent = container.auxiliaryContent
self.menuItems = container.menuItems
super.init()
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint
) -> UIContextMenuConfiguration? {
container.viewModel.contextMenuManager?.notifyOnContextMenuInteraction(
interaction,
configurationForMenuAtLocation: location
)
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
UIMenu(title: "", children: self?.menuItems() ?? [])
}
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
willDisplayMenuFor configuration: UIContextMenuConfiguration,
animator: UIContextMenuInteractionAnimating?
) {
container.viewModel.contextMenuManager?.notifyOnContextMenuInteraction(
interaction,
willDisplayMenuFor: configuration,
animator: animator
)
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
willEndFor configuration: UIContextMenuConfiguration,
animator: UIContextMenuInteractionAnimating?
) {
container.viewModel.contextMenuManager?.notifyOnContextMenuInteraction(
interaction,
willEndFor: configuration,
animator: animator
)
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
guard let targetView = interaction.view else { return nil }
let bubbleWithTail = BubbleWithTailPath()
let customPath = bubbleWithTail.path(in: targetView.bounds)
let parameters = UIPreviewParameters()
parameters.visiblePath = customPath
return UITargetedPreview(
view: targetView,
parameters: parameters
)
}
func onRequestMenuAuxiliaryPreview(sender: ContextMenuManager) -> UIView? {
let hostingController = UIHostingController(rootView: auxiliaryContent)
hostingController.view.backgroundColor = .clear
return hostingController.view
}
}
i am trying to recreate apple's iMessage tapback reaction context menu. i have figured out how to attach an auxiliary view to the view with the menu, however the menu itself varies in position. if i bring up the context menu on a view below half the screen height, the menu appears above the view. else, it appears below. i need it to always be below.
Post
Replies
Boosts
Views
Activity
I am trying to seamlessly transition a view from one view controller and have it "jump" into a sheet.
Apple does this in their widget picker for example when you add one of the top widgets.
Based on all the documentation I've read, the most common approach to this is to snapshot the area/view that you are trying to seamlessly transition in the presented view controller (the place where the item is coming from. And then have that snapshot translate across into where it should be in the layout of the presenting view controller.
func makeAnimatorIfNeeded(
using transitionContext: UIViewControllerContextTransitioning
) -> UIViewPropertyAnimator {
if let animator = animator {
return animator
}
// Presentation animation
let animator = UIViewPropertyAnimator(
duration: 0.5,
timingParameters: UISpringTimingParameters(dampingRatio: 1.0)
)
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else {
transitionContext.completeTransition(false)
return animator
}
/// !IMPORTANT: For some weird reason, accessing the views of the view controller
/// directly, like `fromVC.view` or `toVC.view` will break the sheet transition and gestures.
/// Instead, use the `view(forKey:)` method of the `transitionContext` to get the views.
let fromView = transitionContext.view(forKey: .from)
let toView = transitionContext.view(forKey: .to)
let containerView = transitionContext.containerView
let fromFrame = transitionContext.viewController(forKey: .from).map { transitionContext.finalFrame(for: $0) } ?? containerView.bounds
let toFrame = transitionContext.viewController(forKey: .to).map { transitionContext.finalFrame(for: $0) } ?? containerView.bounds
// Calculate the frame of sourceView relative to fromVC.view to capture the correct snapshot area.
let snapshotFrameInFromVC = sourceView?.convert(sourceView?.frame ?? .zero, to: fromVC.view) ?? containerView.frame
// Calculate the frame of sourceView relative to containerView to position the snapshot correctly.
let snapshotFrameInContainer = sourceView?.convert(sourceView?.frame ?? .zero, to: containerView) ?? containerView.frame
let snapshot: UIView?
if isPresenting {
// Create a snapshot of fromVC.view from the defined area (snapshotFrameInFromVC).
let originalColor = fromVC.view.backgroundColor
fromVC.view.backgroundColor = .clear
snapshot = fromVC.view.resizableSnapshotView(from: snapshotFrameInFromVC,
afterScreenUpdates: true,
withCapInsets: .zero)
fromVC.view.backgroundColor = originalColor
// Position the snapshot correctly within the snapshot container
snapshot?.frame = snapshotFrameInContainer
toView?.frame = toFrame
toView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
toView?.layoutIfNeeded()
if let fromView {
containerView.addSubview(fromView)
}
if let toView {
containerView.addSubview(toView)
containerView.addSubview(snapshot ?? UIView())
}
let toViewCenter = CGPoint(
x: toVC.view.bounds.midX,
y: toVC.view.bounds.midY + 55
)
let gestureVelocity = CGPoint(x: 0, y: -5000)
animator.addAnimations {
Wave.animate(
withSpring: self.animatedSpring,
mode: .animated,
gestureVelocity: gestureVelocity
) {
snapshot?.animator.frame.size = CGSize(width: 204, height: 204) // Important to animate first
snapshot?.animator.center = toViewCenter
} completion: { finished, retargeted in
print("finished: \(finished), retargeted: \(retargeted)")
}
toView?.transform = CGAffineTransform.identity
}
animator.addCompletion { animatingPosition in
switch animatingPosition {
case .end:
snapshot?.removeFromSuperview()
transitionContext.completeTransition(true)
default:
transitionContext.completeTransition(false)
}
}
} else {
// Transitioning view is fromView
if let toView {
containerView.addSubview(toView)
}
if let fromView {
containerView.addSubview(fromView)
}
animator.addAnimations {
fromView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
}
animator.addCompletion { animatingPosition in
switch animatingPosition {
case .end:
transitionContext.completeTransition(true)
default:
transitionContext.completeTransition(false)
}
}
}
self.animator = animator
return animator
}
I can pull this off seamlessly if the animation involves a simple movement from A to B.
But if I try to resize the snapshot as well, the snapshot becomes pixelated and low quality (I assume this is because the snapshot is literally a small screenshot and I'm stretching it from 56x56 to 204x204).
Is there another way that I'm overlooking to pull this off without a snapshot? Or is there a way I can resize the snapshot without losing quality?
Here is the animator code, it works great without the sizing so this should help future people looking to replicate the same effect anyways.
I'm trying to accomplish something like this:
https://x.com/mlaithv/status/1835041850236838265
But it seems like Apple uses a Private API to pull it off. Has anyone managed to create something similar? I know there is a package named Transmission that can do it but it seems hacky & I'm unfamiliar with UIKit. The source uses a "custom modal" but I'm not sure how.
specifically using the newer colorEffect, layerEffect, etc routes. the below does not seem to work. do i have to use the old MTK stuff?
import SwiftUI
import MetalKit
struct HoloPreview: View {
let startDate = Date()
@State private var noiseTexture: MTLTexture?
var body: some View {
TimelineView(.animation) { context in
RoundedRectangle(cornerRadius: 20)
.fill(Color.red)
.layerEffect(ShaderLibrary.iridescentEffect(
.float(startDate.timeIntervalSinceNow),
.texture(noiseTexture)
), maxSampleOffset: .zero)
}
.onAppear {
noiseTexture = loadTexture(named: "perlinNoiseMap")
}
}
func loadTexture(named imageName: String) -> MTLTexture? {
guard let device = MTLCreateSystemDefaultDevice(),
let url = Bundle.main.url(forResource: imageName, withExtension: "png") else {
return nil
}
let textureLoader = MTKTextureLoader(device: device)
let texture = try? textureLoader.newTexture(URL: url, options: nil)
return texture
}
}
#Preview {
HoloPreview()
}
I'm trying to mimic the tapback context menu with a chat message bubble in imessage. i am aware i can use a custom preview but it doesn’t work for my case as there appears to be a gray background i can’t get rid of.
i was told i could use a custom uikit approach but am unsure
Essentially, I'm trying to find the most straightforward/simple way to outline an Image with varying contours. The intention is similar to the way iMessage allows you to add an outline to a sticker. The "goal" in the example is simply the input image on top of the outline.
i'm trying to figure out how to basically engrave some text into this ellipsoid mesh. so far the only thing i've learned that can sort of come close to what im looking for is SCNText but it floats above the ellipsoid and doesnt conform to the angular shape.
let allocator = MTKMeshBufferAllocator(device: MTLCreateSystemDefaultDevice()!)
let disc = MDLMesh.newEllipsoid(
withRadii: vector_float3(Float(discDiameter/2), Float(discDiameter/2), Float(discThickness/2)),
radialSegments: 64,
verticalSegments: 64,
geometryType: .triangles,
inwardNormals: false,
hemisphere: false,
allocator: allocator
)
let discGeometry = SCNGeometry(mdlMesh: disc)
let material = createIridescentMaterial()
discGeometry.materials = [material]
I am new to swift. This is my Item.swift.
import SwiftData
@Model
final class Item: Codable {
var id: String
var soundId: String
var soundAppleId: String
var soundType: String
var type: String
var authorId: String
var text: String
var createdAt: Date
var actionsCount: Int
var chainsCount: Int
var rating: Int
var loved: Bool
var replay: Bool
var heartedByUser: Bool
@Relationship var author: Author?
init(id: String, soundId: String, soundAppleId: String, soundType: String, type: String, authorId: String, text: String, createdAt: Date, actionsCount: Int, chainsCount: Int, ratings: Int, loved: Bool, replay: Bool, heartedByUser: Bool, author: Author) {
self.id = id
self.soundId = soundId
self.soundAppleId = soundAppleId
self.soundType = soundType
self.type = type
self.authorId = authorId
self.text = text
self.createdAt = createdAt
self.actionsCount = actionsCount
self.chainsCount = chainsCount
self.rating = ratings
self.loved = loved
self.replay = replay
self.heartedByUser = heartedByUser
self.author = author
}
private enum CodingKeys: String, CodingKey {
case id
case soundId = "sound_id"
case soundAppleId = "sound_apple_id"
case soundType = "sound_type"
case type
case authorId = "author_id"
case text
case createdAt = "created_at"
case actionsCount = "actions_count"
case chainsCount = "chains_count"
case rating
case loved
case replay
case heartedByUser
case author
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
soundId = try container.decode(String.self, forKey: .soundId)
soundAppleId = try container.decode(String.self, forKey: .soundAppleId)
soundType = try container.decode(String.self, forKey: .soundType)
type = try container.decode(String.self, forKey: .type)
authorId = try container.decode(String.self, forKey: .authorId)
text = try container.decode(String.self, forKey: .text)
let dateString = try container.decode(String.self, forKey: .createdAt)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
if let date = formatter.date(from: dateString) {
createdAt = date
} else {
throw DecodingError.dataCorruptedError(forKey: .createdAt,
in: container,
debugDescription: "Date string does not match format expected by formatter.")
}
actionsCount = try container.decode(Int.self, forKey: .actionsCount)
chainsCount = try container.decode(Int.self, forKey: .chainsCount)
rating = try container.decode(Int.self, forKey: .rating)
loved = try container.decode(Bool.self, forKey: .loved)
replay = try container.decode(Bool.self, forKey: .replay)
heartedByUser = try container.decode(Bool.self, forKey: .heartedByUser)
author = try container.decode(Author.self, forKey: .author)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(soundId, forKey: .soundId)
try container.encode(soundAppleId, forKey: .soundAppleId)
try container.encode(soundType, forKey: .soundType)
try container.encode(type, forKey: .type)
try container.encode(authorId, forKey: .authorId)
try container.encode(text, forKey: .text)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let dateString = formatter.string(from: createdAt)
try container.encode(dateString, forKey: .createdAt)
try container.encode(actionsCount, forKey: .actionsCount)
try container.encode(chainsCount, forKey: .chainsCount)
try container.encode(rating, forKey: .rating)
try container.encode(loved, forKey: .loved)
try container.encode(replay, forKey: .replay)
try container.encode(heartedByUser, forKey: .heartedByUser)
try container.encode(author, forKey: .author)
}
}
@Model
final class Author: Codable {
var id: String
var image: URL
var username: String
var bio: String?
init(id: String, image: URL, username: String, bio: String?) {
self.id = id
self.image = image
self.username = username
self.bio = bio
}
private enum CodingKeys: String, CodingKey {
case id
case image
case username
case bio
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
image = try container.decode(URL.self, forKey: .image)
username = try container.decode(String.self, forKey: .username)
bio = try container.decodeIfPresent(String.self, forKey: .bio)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(image, forKey: .image)
try container.encode(username, forKey: .username)
try container.encodeIfPresent(bio, forKey: .bio)
}
}
In my ItemView when I try to access something inside author, Swift preview crashes.
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 SwiftData 0x1cb459e90 0x1cb3d3000 + 552592
1 SwiftData 0x1cb45ba7c 0x1cb3d3000 + 559740
2 SwiftData 0x1cb45e5f8 0x1cb3d3000 + 570872
3 SwiftData 0x1cb4190e4 0x1cb3d3000 + 286948
4 audition 0x100b436e0 Item.author.getter + 320 (@__swiftmacro_8audition4ItemC6author18_PersistedPropertyfMa_.swift:9)
5 ContentView.1.preview-thunk.dylib 0x105f23a20 closure #1 in closure #1 in closure #1 in closure #1 in ItemCard.__preview__body.getter + 820 (ContentView.swift:89)
6 SwiftUI 0x1cba41308 0x1cb47b000 + 6054664
7 ContentView.1.preview-thunk.dylib 0x105f22ee4 closure #1 in closure #1 in closure #1 in ItemCard.__preview__body.getter + 472 (ContentView.swift:84)
8 SwiftUI 0x1cc2e6c40 0x1cb47b000 + 15121472
9 ContentView.1.preview-thunk.dylib 0x105f228b8 closure #1 in closure #1 in ItemCard.__preview__body.getter + 388 (ContentView.swift:83)
...
export default function MyComponent() {
const { isLoading, error, data } = useQuery(
"songs",
async () => {
const response = await fetch(
"https://api.music.apple.com/v1/catalog/us/search?term=beach+bunny"
);
console.log(response.headers); // log the response headers
const data = await response.json();
return data.results;
},
{
headers: {
Authorization: `Bearer ${"thisiswheremytokenis"}`,
},
}
);
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error fetching data: {error.message}</div>;
}
return (
<div>
<h1>Songs</h1>
<ul>
{data.map((song) => (
<li key={song.id}>{song.attributes.name}</li>
))}
</ul>
</div>
);
}
It's just a simple react component thats using react-query and I cannot for the life of me figure out why it's not working. I know my Token works because when I verify it with the curl command in the docs it gives me code 200.