Post

Replies

Boosts

Views

Created

SwiftUI Canvas ring animation briefly rotates backward after app returns from background
Hi, I have a SwiftUI "work time" screen with a rotating ring (60 tick marks, Canvas-based). While the app stays in foreground, rotation is fine. After the app is in background for a while and comes back to foreground, I consistently see one visual glitch: the ring makes one very short step in the opposite direction once then continues rotating clockwise normally So this is not a crash, only a visual reverse tick on resume. What I expect: no direction change after foreground resume continuous clockwise motion What I already tried: withAnimation(.linear(...).repeatForever(...)) + restart on scenePhase TimelineView (.animation and .periodic) with time-based angle angle with and without modulo wrapping wall-clock and monotonic time sources rotation via rotationEffect and also via Canvas geometry warmup delays after resume restoring original ring visuals (long/short tick marks) The effect is still reproducible. Question: What is the correct SwiftUI approach to implement a continuously rotating ring that stays direction-stable across background/foreground transitions, with no one-frame reverse step on resume? Any pattern that is robust on current iOS versions and avoids visual artifacts on scene phase changes would be appreciated. Minimal repro: import SwiftUI struct ReproClockView: View { @Environment(\.scenePhase) private var scenePhase private let tickCount = 60 private let rotationDuration: Double = 120 @State private var rotationDegrees: Double = 0 @State private var hasAppeared = false var body: some View { ZStack { Canvas { context, size in let center = CGPoint(x: size.width / 2, y: size.height / 2) let radius = min(size.width, size.height) / 2 - 8 for index in 0..<tickCount { let angle = Double(index) * (360.0 / Double(tickCount)) - 90 let radians = angle * .pi / 180 let isLongTick = index % 5 == 0 let length: CGFloat = isLongTick ? 22 : 14 let outerRadius = radius let innerRadius = radius - length let startPoint = CGPoint( x: center.x + cos(radians) * outerRadius, y: center.y + sin(radians) * outerRadius ) let endPoint = CGPoint( x: center.x + cos(radians) * innerRadius, y: center.y + sin(radians) * innerRadius ) var path = Path() path.move(to: startPoint) path.addLine(to: endPoint) context.stroke( path, with: .color(.red), style: StrokeStyle(lineWidth: 2.5, lineCap: .round) ) } } .rotationEffect(.degrees(rotationDegrees)) .drawingGroup() } .frame(width: 340, height: 340) .onAppear { guard !hasAppeared else { return } hasAppeared = true startRotation() } .onChange(of: scenePhase) { oldPhase, newPhase in if oldPhase == .background && newPhase == .active { withAnimation(.linear(duration: 0)) { rotationDegrees = 0 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { startRotation() } } } } private func startRotation() { rotationDegrees = 0 withAnimation(.linear(duration: rotationDuration).repeatForever(autoreverses: false)) { rotationDegrees = 360 } } }
0
0
99
14h
(tvOS) Categories or Selection Menus Don't Fit the Design
Hi everyone, I'm currently working on my own Apple TV app. So far, things are going pretty well, but right now, I'm stuck on the design of the categories or selection menus. Here's a screenshot of how it looks right now: The green color and the border are intentionally added for now so I can see what is where. My actual goal is to remove the gray bar (or is this the "main bar"?). The pink bar and its border are just design elements that can be removed if needed. I want it to look more "original," like this: Here is the code: let title: String let isSelected: Bool var body: some View { HStack { Text(title) .foregroundColor(isSelected ? .black : .white) .font(.system(size: 22, weight: .regular)) .padding(.leading, 20) Spacer() Image(systemName: "chevron.right") .foregroundColor(isSelected ? .black : .gray) .padding(.trailing, 20) } .frame(height: 50) // Einheitliche Höhe für die Kategorien .background(Color.pink) // Innerer Hintergrund auf pink gesetzt .cornerRadius(10) // Abrundung direkt auf den Hintergrund anwenden .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.green, lineWidth: 3) // Äußerer Rahmen auf grün gesetzt ) .padding(.horizontal, 0) // Entferne äußere Ränder .background(Color.clear) // Entferne alle anderen Hintergründe } } struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView() } } I’ve adjusted the code, but it’s still not quite right. When a category is not selected, it appears black instead of gray, like in the original design Here is the code: struct SettingsView: View { @State private var selectedCategory: String? var body: some View { NavigationStack { ZStack { Color.black .edgesIgnoringSafeArea(.all) VStack(spacing: 0) { // Überschrift oben in der Mitte Text("Einstellungen") .font(.system(size: 40, weight: .semibold)) .foregroundColor(.white) .padding(.top, 30) HStack { // Linke Seite mit Logo VStack { Spacer() Image(systemName: "applelogo") .resizable() .scaledToFit() .frame(width: 120, height: 120) .foregroundColor(.white) Spacer() } .frame(width: UIScreen.main.bounds.width * 0.4) // Rechte Seite mit Kategorien VStack(spacing: 15) { ForEach(categories, id: \.self) { category in NavigationLink( value: category, label: { SettingsCategoryView( title: category, isSelected: selectedCategory == category ) } ) .buttonStyle(PlainButtonStyle()) } } .frame(width: UIScreen.main.bounds.width * 0.5) } } } .navigationDestination(for: String.self) { value in Text("\(value)-Ansicht") .font(.title) .foregroundColor(.white) .navigationTitle(value) } } } private var categories: [String] { ["Allgemein", "Benutzer:innen und Accounts", "Video und Audio", "Bildschirmschoner", "AirPlay und HomeKit", "Fernbedienungen und Geräte", "Apps", "Netzwerk", "System", "Entwickler"] } } struct SettingsCategoryView: View { let title: String let isSelected: Bool var body: some View { HStack { Text(title) .foregroundColor(.white) .font(.system(size: 22, weight: .medium)) .padding(.leading, 20) Spacer() Image(systemName: "chevron.right") .foregroundColor(.gray) .padding(.trailing, 20) } .frame(height: 50) // Einheitliche Höhe für die Kategorien .background(isSelected ? Color.gray.opacity(0.3) : Color.clear) // Hervorhebung des ausgewählten Elements .cornerRadius(8) // Abgerundete Ecken .scaleEffect(isSelected ? 1.05 : 1.0) // Fokus-Animation .animation(.easeInOut, value: isSelected) } } struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView() } }
0
0
614
Jan ’25
SwiftUI Canvas ring animation briefly rotates backward after app returns from background
Hi, I have a SwiftUI "work time" screen with a rotating ring (60 tick marks, Canvas-based). While the app stays in foreground, rotation is fine. After the app is in background for a while and comes back to foreground, I consistently see one visual glitch: the ring makes one very short step in the opposite direction once then continues rotating clockwise normally So this is not a crash, only a visual reverse tick on resume. What I expect: no direction change after foreground resume continuous clockwise motion What I already tried: withAnimation(.linear(...).repeatForever(...)) + restart on scenePhase TimelineView (.animation and .periodic) with time-based angle angle with and without modulo wrapping wall-clock and monotonic time sources rotation via rotationEffect and also via Canvas geometry warmup delays after resume restoring original ring visuals (long/short tick marks) The effect is still reproducible. Question: What is the correct SwiftUI approach to implement a continuously rotating ring that stays direction-stable across background/foreground transitions, with no one-frame reverse step on resume? Any pattern that is robust on current iOS versions and avoids visual artifacts on scene phase changes would be appreciated. Minimal repro: import SwiftUI struct ReproClockView: View { @Environment(\.scenePhase) private var scenePhase private let tickCount = 60 private let rotationDuration: Double = 120 @State private var rotationDegrees: Double = 0 @State private var hasAppeared = false var body: some View { ZStack { Canvas { context, size in let center = CGPoint(x: size.width / 2, y: size.height / 2) let radius = min(size.width, size.height) / 2 - 8 for index in 0..<tickCount { let angle = Double(index) * (360.0 / Double(tickCount)) - 90 let radians = angle * .pi / 180 let isLongTick = index % 5 == 0 let length: CGFloat = isLongTick ? 22 : 14 let outerRadius = radius let innerRadius = radius - length let startPoint = CGPoint( x: center.x + cos(radians) * outerRadius, y: center.y + sin(radians) * outerRadius ) let endPoint = CGPoint( x: center.x + cos(radians) * innerRadius, y: center.y + sin(radians) * innerRadius ) var path = Path() path.move(to: startPoint) path.addLine(to: endPoint) context.stroke( path, with: .color(.red), style: StrokeStyle(lineWidth: 2.5, lineCap: .round) ) } } .rotationEffect(.degrees(rotationDegrees)) .drawingGroup() } .frame(width: 340, height: 340) .onAppear { guard !hasAppeared else { return } hasAppeared = true startRotation() } .onChange(of: scenePhase) { oldPhase, newPhase in if oldPhase == .background && newPhase == .active { withAnimation(.linear(duration: 0)) { rotationDegrees = 0 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { startRotation() } } } } private func startRotation() { rotationDegrees = 0 withAnimation(.linear(duration: rotationDuration).repeatForever(autoreverses: false)) { rotationDegrees = 360 } } }
Replies
0
Boosts
0
Views
99
Activity
14h
(tvOS) Categories or Selection Menus Don't Fit the Design
Hi everyone, I'm currently working on my own Apple TV app. So far, things are going pretty well, but right now, I'm stuck on the design of the categories or selection menus. Here's a screenshot of how it looks right now: The green color and the border are intentionally added for now so I can see what is where. My actual goal is to remove the gray bar (or is this the "main bar"?). The pink bar and its border are just design elements that can be removed if needed. I want it to look more "original," like this: Here is the code: let title: String let isSelected: Bool var body: some View { HStack { Text(title) .foregroundColor(isSelected ? .black : .white) .font(.system(size: 22, weight: .regular)) .padding(.leading, 20) Spacer() Image(systemName: "chevron.right") .foregroundColor(isSelected ? .black : .gray) .padding(.trailing, 20) } .frame(height: 50) // Einheitliche Höhe für die Kategorien .background(Color.pink) // Innerer Hintergrund auf pink gesetzt .cornerRadius(10) // Abrundung direkt auf den Hintergrund anwenden .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.green, lineWidth: 3) // Äußerer Rahmen auf grün gesetzt ) .padding(.horizontal, 0) // Entferne äußere Ränder .background(Color.clear) // Entferne alle anderen Hintergründe } } struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView() } } I’ve adjusted the code, but it’s still not quite right. When a category is not selected, it appears black instead of gray, like in the original design Here is the code: struct SettingsView: View { @State private var selectedCategory: String? var body: some View { NavigationStack { ZStack { Color.black .edgesIgnoringSafeArea(.all) VStack(spacing: 0) { // Überschrift oben in der Mitte Text("Einstellungen") .font(.system(size: 40, weight: .semibold)) .foregroundColor(.white) .padding(.top, 30) HStack { // Linke Seite mit Logo VStack { Spacer() Image(systemName: "applelogo") .resizable() .scaledToFit() .frame(width: 120, height: 120) .foregroundColor(.white) Spacer() } .frame(width: UIScreen.main.bounds.width * 0.4) // Rechte Seite mit Kategorien VStack(spacing: 15) { ForEach(categories, id: \.self) { category in NavigationLink( value: category, label: { SettingsCategoryView( title: category, isSelected: selectedCategory == category ) } ) .buttonStyle(PlainButtonStyle()) } } .frame(width: UIScreen.main.bounds.width * 0.5) } } } .navigationDestination(for: String.self) { value in Text("\(value)-Ansicht") .font(.title) .foregroundColor(.white) .navigationTitle(value) } } } private var categories: [String] { ["Allgemein", "Benutzer:innen und Accounts", "Video und Audio", "Bildschirmschoner", "AirPlay und HomeKit", "Fernbedienungen und Geräte", "Apps", "Netzwerk", "System", "Entwickler"] } } struct SettingsCategoryView: View { let title: String let isSelected: Bool var body: some View { HStack { Text(title) .foregroundColor(.white) .font(.system(size: 22, weight: .medium)) .padding(.leading, 20) Spacer() Image(systemName: "chevron.right") .foregroundColor(.gray) .padding(.trailing, 20) } .frame(height: 50) // Einheitliche Höhe für die Kategorien .background(isSelected ? Color.gray.opacity(0.3) : Color.clear) // Hervorhebung des ausgewählten Elements .cornerRadius(8) // Abgerundete Ecken .scaleEffect(isSelected ? 1.05 : 1.0) // Fokus-Animation .animation(.easeInOut, value: isSelected) } } struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView() } }
Replies
0
Boosts
0
Views
614
Activity
Jan ’25