I understand two key concepts from desktop platforms:
Screen Mirroring – The same content is displayed on both the primary and external screens.
Screen Extension – The external display shows different content that complements what's on the main screen.
My question pertains to the second point: Is it possible to extend the display on iOS and iPadOS devices?
I'm referring to this Apple documentation, which explains how to extend content from an iOS/iPadOS device to an external display.
I tested this in a sample iOS Xcode project. In the iOS Simulator, I was able to detect an "external display" and present a separate UIWindow on it. However, when I tried the same on a real device (iPhone 15 connected to a MacBook Pro via cable), the external display connection was not detected.
I’d like to confirm whether screen extension is possible on a real iOS device. From my research, it appears that extension is only supported on iPadOS via Stage Manager, but I want to verify if there’s any way to achieve this on an iPhone. If so, are there any known apps that currently utilize extended display functionality on iOS?
If extension is not possible on iOS, what does the documentation mentions iOS?
Explore the various UI frameworks available for building app interfaces. Discuss the use cases for different frameworks, share best practices, and get help with specific framework-related questions.
Post
Replies
Boosts
Views
Activity
I want to be able to dynamically update the phrase dictionary in an AppShortcut. However, whenever I abstract the phrases, the shortcut fails to display. That is, I am trying to do:
static var phrases: [AppShortcutPhrase<MyIntent>] = ["\(.applicationName) hello world"]
AppShortcut(
intent: MyIntent(),
phrases: phrases,
shortTitle: "hello world",
systemImageName: ""
)
However, the following works:
AppShortcut(
intent: MyIntent(),
phrases: "\(.applicationName) hello world",
shortTitle: "hello world",
systemImageName: ""
)
So, what gives?
If you add the .scaleEffect() modifier to a parent view inside of which there are children with contextMenu()-s, the context menu preview unfortunately keeps the original, unscaled size of the child view. This results in a very weird, glitchy user experience.
Unfortunately, providing a custom preview that is scaled up also does not help, since even though it is the right size, it gets cropped to the unscaled size of the child view.
Adding scaleEffect() to each child element individually (BEFORE the contextMenu() modifier!) does make the problem disappear. However, I would like to avoid this, since my use case is zooming into a complex graph with context menus on its nodes, and having to recalculate the position of each node manually seems to perform much worse than delegating that work to scaleEffect().
Tested on iOS 18.2 (device + emulator)
Is there a workaround?
Here is a minimal working example that demonstrates the problem:
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Rectangle()
.frame(width: 100, height: 100)
.contextMenu {
Button("Test") {}
Button("Test") {}
}
Rectangle()
.frame(width: 100, height: 100)
.contextMenu {
Button("Test") {}
Button("Test") {}
}
}
.scaleEffect(1.5)
}
}
Screenshot (problem: The two squares are the same size. However, the long-tapped upper square got shrunk down before the context menu got displayed.)
In this setup, label do not show properly because of the textColor.
Labels are defined in IB, in the following hierarchy:
ViewController
View
Label 1
scrollView
View
Button
Label 2
Buttons show properly, but labels, even though defined with default label color appear as if their alpha was 0.2. It is even worse in dark mode:
I have checked the settings for the label and did not find anything anormal:
I have tried to change label color to system.gray 2, to no avail. If I change to red, does not show in red in IB.
Problem appears for both Label 1 (at the top level in the view) and Label 2
The DeviceActivityReport view does not render immediately when added to the view hierarchy. Instead, it requires repeated navigation to the screen hosting the DeviceActivityReport view for it to appear.
Furthermore, there is no programmatic way to determine whether the view is being rendered for the user, leading to an inconsistent and often poor user experience.
I've created a sample project that demonstrates the issue.
I'm testing using Group Activities and having no trouble iOS<->iOS or starting an activity on macOS and joining via iOS. However, when I start an activity and then try to join it from another macOS client, the starting side joins the session just fine, but the receiving side acts like I don't have the required app, even when it is already running.
I see the active SharePlay icon in the menu bar, and the Current Activity is shown, but instead of an "Open" button there is a "MyApp Required" string and a "View" button that goes to the App Store. (Where the app is not available yet, as expected, since I'm still working on it.) There is no GroupSession started on that Mac yet, obviously.
I'm looking for any hints to help debug what is going on. How does Group Activities find the app for the activity on macOS and how can I figure out why it isn't finding mine?
Thanks!
I plan to use the entire screen height minus 40 pixels approximately to not overlap with the time, batter and carrier data. However, I noticed that in the code shared below the vstack with pink background is not displayed at the top of the screen. The interesting part is that it's actually occupying an offset at the top of the screen. What's more, when I set an offset greater than 70 pixels, then the pink vstack displays on the view! Thus, I'm looking for an explanation to this swiftui rendering issue.
Offset less than 70 pixels:
Offset greater or equal than 70 pixels:
GeometryReader { proxy in
let offset = 40.0
let height = proxy.size.height - offset
ZStack {
VStack(spacing:0){
VStack{Text("heasdas")}.frame(width: 300,height: offset,alignment: .leading)
.background(.pink)
VStack {
HStack(alignment:.center,spacing:10){
Text("Shapecloud")
.font(.callout)
.fontWeight(.semibold)
.frame(alignment: .leading)
SLine()
}
.frame(maxWidth: .infinity,alignment: .leading)
Text("Digital Twin Solutions\nServices")
.font(.largeTitle)
.fontWeight(.medium)
.frame(maxWidth: .infinity,alignment: .leading)
}
.frame(maxWidth: .infinity,maxHeight: 0.3*height,alignment: .top)
.background(.red)
VStack {
VideoPlayer(player: player)
.frame(maxWidth: .infinity,maxHeight: 300)
}
.frame(maxWidth: .infinity,maxHeight: 0.4*height)
.background(.yellow)
VStack{
Button {
} label: {
Text("Subscribe now").foregroundStyle(.black)
.font(.headline)
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity,maxHeight: 50)
.border(Color.black, width: 2)
Button {
} label: {
Text("Sign In").foregroundStyle(.white)
.font(.headline)
}
.frame(maxWidth: .infinity,maxHeight: 50)
.background(Color.theme.primary)
}
.frame(maxWidth: .infinity,maxHeight: 0.3*height)
.background(.green)
}
.padding(.horizontal, 32)
.background(.cyan)
.ignoresSafeArea(.all)
}
.ignoresSafeArea(.all)
}
.ignoresSafeArea(.all)
I'm trying to setup a widget to pull an image down from a webserver and I'm running into an error of Widget archival failed due to image being too large [9] - (1024, 1024), totalArea: 1048576 > max[718080.000000].
I've tried two different approaches to resolve this error and both have failed to resolve the image.
I've also confirmed that I'm getting the image in the AppIntentTimelineProvider.
private func getImageUI(urlString: String) -> UIImage? {
guard let url = URL(string: urlString) else { return nil }
guard let imageData = try? Data(contentsOf: url) else { return nil }
return UIImage(data: imageData)?.resizedForWidget()
}
Is there another approach I could take on addressing this issue so the image appears on the widget?
Simple approach
extension UIImage {
func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))
let format = imageRendererFormat
format.opaque = isOpaque
return UIGraphicsImageRenderer(size: canvas, format: format).image {
_ in draw(in: CGRect(origin: .zero, size: canvas))
}
}
}
extension UIImage {
/// Resize the image to strictly fit within WidgetKit’s max allowed pixel area (718,080 pixels)
func resizedForWidget(maxArea: CGFloat = 718_080.0, isOpaque: Bool = true) -> UIImage? {
let originalWidth = size.width
let originalHeight = size.height
let originalArea = originalWidth * originalHeight
print("🔍 Original Image Size: \(originalWidth)x\(originalHeight) → Total Pixels: \(originalArea)")
// ✅ If the image is already within the limit, return as is
if originalArea <= maxArea {
print("✅ Image is already within the allowed area.")
return self
}
// 🔄 Calculate the exact scale factor to fit within maxArea
let scaleFactor = sqrt(maxArea / originalArea)
let newWidth = floor(originalWidth * scaleFactor) // Use `floor` to ensure area is always within limits
let newHeight = floor(originalHeight * scaleFactor)
let newSize = CGSize(width: newWidth, height: newHeight)
print("🛠 Resizing Image: \(originalWidth)x\(originalHeight) → \(newWidth)x\(newHeight)")
// ✅ Force bitmap rendering to ensure the resized image is properly stored
let format = UIGraphicsImageRendererFormat()
format.opaque = isOpaque
format.scale = 1 // Ensures we are not letting UIKit auto-scale it back up
let renderer = UIGraphicsImageRenderer(size: newSize, format: format)
let resizedImage = renderer.image { _ in
self.draw(in: CGRect(origin: .zero, size: newSize))
}
print("✅ Final Resized Image Size: \(resizedImage.size), Total Pixels: \(resizedImage.size.width * resizedImage.size.height)")
return resizedImage
}
}
These are logs from a failed image render if that helps
🔍 Original Image Size: 720.0x1280.0 → Total Pixels: 921600.0
🛠 Resizing Image: 720.0x1280.0 → 635.0x1129.0
✅ Final Resized Image Size: (635.0, 1129.0), Total Pixels: 716915.0
When integrating SwiftData for an already existing app that uses CoreData as data management, I encounter errors.
When building the ModelContainer for the first time, the following error appears:
Error: Persistent History (184) has to be truncated due to the following entities being removed (all Entities except for the 2 where I defined a SwiftData Model)
class SwiftDataManager: ObservableObject {
static let shared = SwiftDataManager()
private let persistenceManager = PersistenceManager.shared
private init(){}
lazy var modelContainer: ModelContainer = {
do {
let storeUrl = persistenceManager.storeURL()
let schema = Schema([
HistoryIncident.self,
HistoryEvent.self
])
let modelConfig = ModelConfiguration(url: storeUrl)
return try ModelContainer(for: schema, configurations: [modelConfig])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
}
@Model public class HistoryIncident {
var missionNr: String?
@Relationship(deleteRule: .cascade) var events: [HistoryEvent]?
public init(){}
}
@Model class HistoryEvent {
var decs: String?
var timestamp: Date?
init(){}
}
As soon as I call the following function.
func addMockEventsToCurrentHistorie() {
var descriptor = FetchDescriptor<HistoryIncident>()
let key = self.hKey ?? ""
descriptor.predicate = #Predicate { mE in
key == mE.key
}
let historyIncident = try? SwiftDataManager.shared.modelContext.fetch(descriptor).first
guard var events = historyIncident?.events else {return}
events.append(contentsOf: createEvents())
}
I get the error:
CoreData: error: (1) I/O error for database at /var/mobile/Containers/Data/Application/55E9D59D-48C4-4D86-8D9F-8F9CA019042D/Library/ Private Documents/appDatabase.sqlite. SQLite error code:1, 'no such column: t0.Z1EVENTS'
/var/mobile/Containers/Data/Application/55E9D59D-48C4-4D86-8D9F-8F9CA019042D/Library/ Private Documents/appDatabase.sqlite. SQLite error code:1, 'no such column: t0.Z1EVENTS' with userInfo of { NSFilePath = "/var/mobile/Containers/Data/Application/55E9D59D-48C4-4D86-8D9F-8F9CA019042D/Library/ Private Documents/appDatabase.sqlite"; NSSQLiteErrorDomain = 1; }
Since iOS 18.3, icons are no longer generated correctly with QLThumbnailGenerator.
No error is returned either.
But this error message now appears in the console:
Error returned from iconservicesagent image request: <ISTypeIcon: 0x3010f91a0>,Type: com.adobe.pdf - <ISImageDescriptor: 0x302f188c0> - (36.00, 36.00)@3x v:1 l:5 a:0:0:0:0 t:() b:0 s:2 ps:0 digest: B19540FD-0449-3E89-AC50-38F92F9760FE error: Error Domain=NSOSStatusErrorDomain Code=-609 "Client is disallowed from making such an icon request" UserInfo={NSLocalizedDescription=Client is disallowed from making such an icon request}
Does anyone know this error? Is there a workaround?
Are there new permissions to consider?
Here is the code how icons are generated:
let request = QLThumbnailGenerator.Request(fileAt: url, size: size, scale: scale, representationTypes: self.thumbnailType)
request.iconMode = true
let generator = QLThumbnailGenerator.shared
generator.generateRepresentations(for: request) { [weak self] thumbnail, _, error in
}
Hello, I have the following code:
.sheet(isPresented: $viewModel.isExerciseSelected) {
ExerciseEditSheetView(viewModel: viewModel)
.presentationDetents([.fraction(0.4)])
}
This code correctly sizes the sheet on my iOS 18 simulator, but does not work on my iOS 17 simulator as well as my actual phone running iOS 17. Instead the sheet always fullscreens. Although its not the end of the world it is pretty annoying. Is there anything I'm missing?
Thanks in advance.
I'm getting the following error Error Domain=com.google.GIDSignIn Code=-4. How do I find out what I'm missing with my setup of Google Cloud?
Hello everyone. I'm building a simple Form in a Multiplatform App with SwiftUI. Originally I had something like this.
import SwiftUI
struct OnboardingForm: View {
@State var firstName: String = ""
@State var lastName: String = ""
@State var email: String = ""
@State var job: String = ""
@State var role: String = ""
var body: some View {
Form {
TextField("First Name", text: $firstName, prompt: Text("Required"))
TextField("Last Name", text: $lastName, prompt: Text("Required"))
TextField("Email", text: $email, prompt: Text("Required"))
TextField("Job", text: $job, prompt: Text("Required"))
TextField("Role", text: $role, prompt: Text("Required"))
}
}
}
#Preview {
OnboardingForm()
}
In macOS it looks ok but then in iOS it looks like this:
and it's impossible to know what each field is for if all the prompts are the same. I tried adding LabeledContent around each text field and that solves it for iOS but then on macOS it looks like this:
The labels are shown twice and the columns are out of alignment. I think I could get around it by doing something like this:
#if os(iOS)
LabeledContent {
TextField("First Name", text: $firstName, prompt: Text("Required"))
} label: {
Text("First Name")
}
#else
TextField("First Name", text: $firstName, prompt: Text("Required"))
#endif
but it seems to me like reinventing the wheel. Is there a "correct" way to declare TextFields with labels that works for both iOS and macOS?
I am creating an application that uses VNDetectBarcodesRequest to read QR codes from images and adjust the image orientation to match that of the QR code finder pattern.
The QR code was successfully read, and the coordinates of the QR code were obtained.Upon checking the obtained topLeft, topRight, and bottomLeft coordinates, they always seem to match the topLeft, topRight, and bottomLeft coordinates of the finder pattern.
Is it specified that the coordinates of topLeft, topRight, and bottomLeft obtained with VNDetectBarcodesRequest match the topLeft, topRight, and bottomLeft of the finder pattern? Or do they just happen to match?
I would appreciate it if you could tell me if the matching of coordinates is a specification.
Thank you for your help.
With "Requires full screen" Split View and Slide Over are disabled but the line on the bottom of the screen remains.
How can that line removed as when a video is displayed full screen?
I would like to report a memory leak issue in watchOS 11.2 that occurs when using .navigationTitle() inside a sheet. This behavior is reproducible both on the simulator and on a real device, but not on iOS. While this does not register as a leak in Instruments, the deinit of the DetailsViewModel is never called, and multiple instances of the view model accumulate in the Memory Graph Debugger. Commenting out .navigationTitle("Sheet View") resolves the issue, and deinit prints as expected. Using @MainActor on the DetailsViewModel does not fix the issue. Nor does switching to @StateObject and using ObservableObject resolve the memory retention.
This issue seems related to other SwiftUI memory leaks that have been reported:
https://developer.apple.com/forums/thread/738840
https://developer.apple.com/forums/thread/736110?login=true&page=1#769898022
https://developer.apple.com/forums/thread/737967?answerId=767599022#767599022
Feedback Number: FB16442048
struct MainView: View {
var body: some View {
NavigationStack {
NavigationLink("Details", value: 1)
.navigationDestination(for: Int.self) { _ in
DetailsView()
}
}
}
}
struct SheetObject: Identifiable {
let id = UUID()
let date: Date
let value: Int
}
@Observable
@MainActor
final class DetailsViewModel {
var sheetObject: SheetObject?
init() {
print("Init")
}
deinit {
print("Deinit")
}
func onAppear() async {
try? await Task.sleep(for: .seconds(2))
sheetObject = .init(date: .now, value: 1)
}
}
struct DetailsView: View {
@State private var viewModel = DetailsViewModel()
@Environment(\.dismiss) var dismiss
var body: some View {
Text("Detail View. Going to sheet, please wait...")
.task {
await viewModel.onAppear()
}
.sheet(item: $viewModel.sheetObject) { sheetObject in
SheetView(sheetObject: sheetObject)
.onDisappear {
dismiss()
}
}
}
}
struct SheetView: View {
let sheetObject: SheetObject
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationStack {
VStack {
Text(sheetObject.date.formatted())
Text(sheetObject.value.formatted())
Button("Dismiss") {
dismiss()
}
}
.navigationTitle("Sheet View") // This line causes a memory leak. Commenting out, you will see "Deinit" be printed.
}
}
}
Prime Objective
I am trying to have a scroll view with a fixed header, a fixed footer, and a WKWebView in between. Using JavaScript, the height of the webView is determined and set to be large enough to hold the entire content.
The Problem
When selecting text on the webView, the view does not scroll when the edges are reached (this works if the webView is shown without being embedded in a Scroll view, or if it is the last element)
What did I try?
I tried reading the scroll view, or adding a gesture recognizer, but all of that does not work because the selection is essentially a system task
Sourcecode
Sourcecode to demonstrate the issue can be found on GitHub
Description
I'm developing a tvOS application where I utilize a UISearchController embedded within a UISearchContainerViewController for search functionality. In a particular flow, a custom view controller contains a TVDigitEntryViewController as a child, with its modalPresentationStyle set to .blurOverFullScreen. The issue arises when a user initiates the PIN entry but decides to cancel and return to the search interface without entering a PIN. Upon returning, the search keyboard is no longer visible, and attempts to focus or interact with it are unsuccessful.
Steps to Reproduce
Initialize and present a UISearchContainerViewController that contains a UISearchController with a results view controller.
Within the search results, present a custom view controller containing TVDigitEntryViewController as a child, setting its modalPresentationStyle to .blurOverFullScreen.
Dismiss the custom view controller without entering a PIN (e.g., by pressing the Menu button on the remote).
Observe that upon returning to the search interface, the keyboard is missing, and focus interactions are unresponsive.
Observed Behavior
After dismissing the custom view controller with TVDigitEntryViewController, the search keyboard does not reappear, and the focus system seems to lose track of the search input field.
Expected Behavior
The search keyboard should remain visible and functional after dismissing the custom view controller, allowing users to continue their search without interruption.
Additional Context
I have reviewed the TVDigitEntryViewController documentation (developer.apple.com) and related discussions on the Apple Developer Forums but have not found a solution to this specific issue.
Questions
Has anyone encountered a similar issue or have insights into why the search keyboard becomes unresponsive after dismissing a .blurOverFullScreen modal with a child TVDigitEntryViewController?
Are there recommended practices to ensure the search keyboard remains active and focusable after such modal presentations?
Any guidance or suggestions would be greatly appreciated. Thank you!
I have a complex app that requires the main SwiftUI view of the app to be embedded inside an NSHostingView which is a subview of an NSViewController's view. Then this NSViewController is wrapped using NSViewControllerRepresentable to be presented using SwiftUI's Window. And if I have a TimelineView inside my SwiftUI view hierarchy, it causes constant recalculation of the layout.
Here's a simplified demo code:
@main
struct DogApp: App {
private let dogViewController = DogViewController()
var body: some Scene {
Window("Dog", id: "main") {
DogViewControllerUI()
}
}
}
private struct DogViewControllerUI: NSViewControllerRepresentable {
let dogViewController = DogViewController ()
func makeNSViewController(context: Context) -> NSViewController { dogViewController }
func updateNSViewController(_ nsViewController: NSViewController, context: Context) {}
func sizeThatFits(_ proposal: ProposedViewSize, nsViewController: NSViewController, context: Context) -> CGSize? {
debugPrint("sizeThatFits", proposal)
return nil
}
}
public class DogViewController: NSViewController {
public override func viewDidLoad() {
super.viewDidLoad()
let mainView = MainView()
let hostingView = NSHostingView(rootView: mainView)
view.addSubview(hostingView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
hostingView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
hostingView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
hostingView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}
struct MainView: View {
var body: some View {
VStack {
TimelineView(.animation) { _ in
Color.random
.frame(width: 100, height: 100)
}
}
}
}
extension Color {
static var random: Color {
Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}
When running it's printing out this repeatedly (multiple times a second).
"sizeThatFits" SwiftUI.ProposedViewSize(width: Optional(559.0), height: Optional(528.0))
"sizeThatFits" SwiftUI.ProposedViewSize(width: Optional(0.0), height: Optional(0.0))
"sizeThatFits" SwiftUI.ProposedViewSize(width: Optional(559.0), height: Optional(528.0))
"sizeThatFits" SwiftUI.ProposedViewSize(width: Optional(0.0), height: Optional(0.0))
"sizeThatFits" SwiftUI.ProposedViewSize(width: Optional(559.0), height: Optional(528.0))
"sizeThatFits" SwiftUI.ProposedViewSize(width: Optional(0.0), height: Optional(0.0))
"sizeThatFits" SwiftUI.ProposedViewSize(width: Optional(559.0), height: Optional(528.0))
If I run an equivalent code for an iPad, it only prints twice. If I comment out TimelineView on macOS, then it only prints out the above logs when resizing the app window.
The main reason this is an issue is that it's clearly causing dramatic degradation in performance. I was told to submit a bug report after I submitted TSI so a SwiftUI engineer could investigate it. Case-ID: 7461887. FB13810482. This was back in May but I received no response. LLMs are no help, and I've experimented with all sorts of workarounds. My last hope is this forum, maybe someone has an idea of what might be going on and why the recalculation is happening constantly on macOS.
Summary
I have a SwiftUI Chart that worked correctly in iOS 17, allowing both horizontal scrolling and tap gesture selection. However, in iOS 18, the same exact chart will not allow for both tap gestures and scrolling to work -- it's either we allow scrolling or we allow tap gestures but not both. We have tried everything to try to circumvent this issue but have had to resort to old methods of creating the chart. This is an issue that has negatively impacted our customers as well.
Again, the charts were working fine on iOS 17, but on iOS 18 the chart scroll + tap gesture capability is not working.
Expected Behavior (iOS 17)
Users can scroll horizontally through the chart.
Users can tap on data points to highlight them.
The selected data point updates when tapped.
Observed Behavior (iOS 18)
The chart no longer scrolls when chartOverlay with the Tap Gesture is applied.
Tap selection still works as expected.
Code Snippet
Below is the working implementation from iOS 17:
private var iOS17ChartView: some View {
Chart {
RectangleMark(
yStart: .value(String(firstLevelAlertBand), firstLevelAlertBand),
yEnd: .value("100", 100)
)
.foregroundStyle(Theme.Colors.green.opacity(0.15))
RectangleMark(
yStart: .value(String(secondLevelAlertBand), secondLevelAlertBand),
yEnd: .value(String(firstLevelAlertBand), firstLevelAlertBand)
)
.foregroundStyle(Theme.Colors.orange.opacity(0.15))
RectangleMark(
yStart: .value("0", 0),
yEnd: .value(String(secondLevelAlertBand), secondLevelAlertBand)
)
.foregroundStyle(Theme.Colors.red.opacity(0.15))
ForEach(telemetryData, id: \.timestamp) { entry in
if let utcDate = dateFormatter.date(from: entry.timestamp) {
let localDate = convertToUserTimeZone(date: utcDate)
let tankLevel = entry.tankLevel ?? 0
LineMark(
x: .value("Date", localDate),
y: .value("Tank Level", tankLevel)
)
.foregroundStyle(statusColor)
AreaMark(
x: .value("Date", localDate),
y: .value("Tank Level", tankLevel)
)
.foregroundStyle(statusColor.opacity(0.50))
PointMark(
x: .value("Date", localDate),
y: .value("Tank Level", tankLevel)
)
.foregroundStyle(selectedDataPoint?.date == localDate ? Theme.Colors.primaryColor : statusColor)
.symbolSize(selectedDataPoint?.date == localDate ? 120 : 80)
PointMark(
x: .value("Date", localDate),
y: .value("Tank Level", tankLevel)
)
//.foregroundStyle(.white).symbolSize(10)
.foregroundStyle(Theme.Colors.white(colorScheme: colorScheme))
.symbolSize(12)
}
}
}
.chartXScale(domain: (firstTimestamp ?? Date())...(latestTimestamp ?? Date()))
.chartXVisibleDomain(length: visibleDomainSize)
.chartScrollableAxes(.horizontal)
.chartScrollPosition(x: $chartScrollPositionX)
.chartXAxis {
AxisMarks(values: .stride(by: xAxisStrideUnit, count: xAxisCount())) { value in
if let utcDate = value.as(Date.self) {
let localDate = convertToUserTimeZone(date: utcDate)
let formatStyle = self.getFormatStyle(for: interval)
AxisValueLabel {
Text(localDate, format: formatStyle)
.font(Theme.Fonts.poppinsRegularExtraSmall)
.foregroundStyle(Theme.Colors.black(colorScheme: colorScheme))
}
AxisTick()
.foregroundStyle(Theme.Colors.black(colorScheme: colorScheme).opacity(1))
}
}
}
.chartOverlay { proxy in
GeometryReader { geometry in
Rectangle().fill(Color.clear).contentShape(Rectangle())
.onTapGesture { location in
let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x
// Use proxy to get the x-axis value at the tapped position
if let selectedDate: Date = proxy.value(atX: xPosition) {
if let closestEntry = telemetryData.min(by: { abs(dateFormatter.date(from: $0.timestamp)!.timeIntervalSince1970 - selectedDate.timeIntervalSince1970) < abs(dateFormatter.date(from: $1.timestamp)!.timeIntervalSince1970 - selectedDate.timeIntervalSince1970) }) {
selectedDataPoint = (convertToUserTimeZone(date: dateFormatter.date(from: closestEntry.timestamp)!), closestEntry.tankLevel ?? 0)
if let dateXPos = proxy.position(forX: convertToUserTimeZone(date: dateFormatter.date(from: closestEntry.timestamp)!)),
let tankLevelYPos = proxy.position(forY: closestEntry.tankLevel ?? 0) {
// Offset the x-position based on the scroll position
let adjustedXPos = dateXPos - proxy.position(forX: chartScrollPositionX)!
withAnimation(.spring()) {
selectedPointLocation = CGPoint(x: adjustedXPos, y: tankLevelYPos - 60) // Offset popup above the point
showPopup = true
}
}
}
}
}
}
.onChange(of: chartScrollPositionX) { newValue in
// Dynamically update the popup position when scroll changes
if let selectedDataPoint = selectedDataPoint {
if let dateXPos = proxy.position(forX: selectedDataPoint.date) {
let adjustedXPos = dateXPos - proxy.position(forX: chartScrollPositionX)!
selectedPointLocation.x = adjustedXPos
}
}
}
}
}
Please help!
Nick