Presenting a sheet over a NavigationStack crashes on macOS but not iPadOS - macos

Iā€™m trying to present a sheet from a pushed view on a NavigationStack on macOS, but it crashes. The same code seems to behave as expected on iPadOS.
struct ContentView: View {
var body: some View {
NavigationSplitView
{
List
{
Text("Foo")
Text("Bar")
}
.navigationSplitViewColumnWidth(min: 200, ideal: 200)
}
detail:
{
NavigationStack
{
Text("The Detail")
.navigationTitle("Detail")
.toolbar
{
ToolbarItemGroup
{
NavigationLink(value: "New thing")
{
Label("New thing", systemImage: "plus")
}
}
}
.navigationDestination(for: String.self)
{ _ in
AddView()
}
}
}
}
}
struct
AddView : View
{
var
body: some View
{
Text("new thing editor")
.navigationTitle("New Thing")
.toolbar
{
ToolbarItemGroup
{
Button(action: { self.isPresentingSearch = true })
{
Label("Search Thing", systemImage: "magnifyingglass")
}
}
}
.sheet(isPresented: self.$isPresentingSearch)
{
Text("Search Thing UI")
}
}
#State private var isPresentingSearch = false
}
Clicking the magnifying glass to set isPresentingSearch true results in:
2023-02-14 19:09:55.539599-0800 TestSheet[34435:770832] [General] The window has been marked as needing another Update Constraints in Window pass, but it has already had more Update Constraints in Window passes than there are views in the window.
<_TtC7SwiftUIP33_00AAB847C3060B4FF7A299E87C10010C23SheetPresentationWindow: 0x15610d0e0> 0x5c3 (1475) {{0, 0}, {97, 44}} en Future marking as needing Update Constraints in Window might be ignored.
2023-02-14 19:09:55.546262-0800 TestSheet[34435:770832] [General] The window has been marked as needing another Update Constraints in Window pass, but it has already had more Update Constraints in Window passes than there are views in the window.
<_TtC7SwiftUIP33_00AAB847C3060B4FF7A299E87C10010C23SheetPresentationWindow: 0x15610d0e0> 0x5c3 (1475) {{0, 0}, {97, 44}} en
2023-02-14 19:09:55.551251-0800 TestSheet[34435:770832] [General] (
0 CoreFoundation 0x00000001a49f83e8 __exceptionPreprocess + 176
1 libobjc.A.dylib 0x00000001a4542ea8 objc_exception_throw + 60
2 CoreFoundation 0x00000001a49f828c +[NSException exceptionWithName:reason:userInfo:] + 0
3 AppKit 0x00000001a7c337a4 -[NSWindow(NSDisplayCycle) _postWindowNeedsUpdateConstraintsUnlessPostingDisabled] + 1844
4 AppKit 0x00000001a7c1d790 -[NSView _informContainerThatSubviewsNeedUpdateConstraints] + 64
5 AppKit 0x00000001a7c1d714 -[NSView setNeedsUpdateConstraints:] + 460
6 SwiftUI 0x00000001cb7944c8 OUTLINED_FUNCTION_19 + 254512
7 SwiftUI 0x00000001cb764df4 OUTLINED_FUNCTION_19 + 60252
.
.
.
App project here: https://github.com/JetForMe/TestSheet

Related

Animation doesn't work on AnyTransition SwiftUI

I'm currently using SwiftUI on a function where a list of images change automatically with opacity animation for each image.
While I've currently managed to get a transition going, the opacity animation does not work no matter what I try.
Could someone please help me out with this...
The code I'm working on is as follows:
//
// EpilogueThree.swift
//
import SwiftUI
struct EpilogueThree: View {
let images = ["1", "2", "3"]
let imageChangeTimer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
let transition = AnyTransition.asymmetric(insertion: .slide, removal: .scale).combined(with: .opacity)
#State private var imagePage = 2
#State private var currentImageIndex = 0
var body: some View {
ZStack {
VStack {
Text("Page: \(imagePage)")
.font(.largeTitle)
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 5)
.background(.black.opacity(0.75))
Image(images[currentImageIndex])
.resizable()
.ignoresSafeArea()
.transition(transition)
.onReceive(imageChangeTimer) { _ in
if imagePage > 0 {
self.currentImageIndex = (self.currentImageIndex + 1) % self.images.count
imagePage -= 1
}
}
}
}
}
}
struct EpilogueThree_Previews: PreviewProvider {
static var previews: some View {
EpilogueThree()
.previewInterfaceOrientation(.landscapeRight)
}
}
The current code acts something like this:
But the code I want needs to do something like this:
We can make image removable in this case by add id (new id - new view) and add animation to container... for animation.
Here is fixed part. Tested with Xcode 13.4 / iOS 15.5
VStack {
// .. other code
Image(images[currentImageIndex])
.resizable()
.ignoresSafeArea()
.id(imagePage) // << here !!
.transition(transition)
.onReceive(imageChangeTimer) { _ in
if imagePage > 0 {
self.currentImageIndex = (self.currentImageIndex + 1) % self.images.count
imagePage -= 1
}
}
}
.animation(.default, value: imagePage) // << here !!

View Modifier messing with animations

I've been looking to add a loading indicator to my project, and found a really cool animation here. To make it easier to use, I wanted to incorporate it into a view modifier to put it on top of the current view. However, when I do so, it doesn't animate when I first press the button. I have played around with it a little, and my hypothesis is that the View Modifier doesn't pass the initial isAnimating = false, so only passes it isAnimating = true when the button is pressed. Because the ArcsAnimationView doesn't get the false value initially, it doesn't actually animate anything and just shows the static arcs. However, if I press the button a second time afterwards, it seems to be initialized and the view properly animates as desired.
Is there a better way to structure my code to avoid this issue? Am I missing something key? Any help is greatly appreciated.
Below is the complete code:
import SwiftUI
struct ArcsAnimationView: View {
#Binding var isAnimating: Bool
let count: UInt = 4
let width: CGFloat = 5
let spacing: CGFloat = 2
init(isAnimating: Binding<Bool>) {
self._isAnimating = isAnimating
}
var body: some View {
GeometryReader { geometry in
ForEach(0..<Int(count)) { index in
item(forIndex: index, in: geometry.size)
// the rotation below is what is animated ...
// I think the problem is that it just starts at .degrees(360), instead of
// .degrees(0) as expected, where it is then animated to .degrees(360)
.rotationEffect(isAnimating ? .degrees(360) : .degrees(0))
.animation(
Animation.default
.speed(Double.random(in: 0.05...0.25))
.repeatCount(isAnimating ? .max : 1, autoreverses: false)
, value: isAnimating
)
.foregroundColor(Color(hex: AppColors.darkBlue1.rawValue))
}
}
.aspectRatio(contentMode: .fit)
}
private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
Group { () -> Path in
var p = Path()
p.addArc(center: CGPoint(x: geometrySize.width/2, y: geometrySize.height/2),
radius: geometrySize.width/2 - width/2 - CGFloat(index) * (width + spacing),
startAngle: .degrees(0),
endAngle: .degrees(Double(Int.random(in: 120...300))),
clockwise: true)
return p.strokedPath(.init(lineWidth: width))
}
.frame(width: geometrySize.width, height: geometrySize.height)
}
}
struct ArcsAnimationModifier: ViewModifier {
#Binding var isAnimating: Bool
func body(content: Content) -> some View {
ZStack {
if isAnimating {
ArcsAnimationView(isAnimating: _isAnimating)
.frame(width: 150)
}
content
.disabled(isAnimating)
}
}
}
extension View {
func loadingAnimation(isAnimating: Binding<Bool>) -> some View {
self.modifier(ArcsAnimationModifier(isAnimating: isAnimating))
}
}
Here is where I actually call the function:
struct AnimationView: View {
#State var isAnimating = false
var body: some View {
VStack {
Button(action: {
self.isAnimating = true
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.isAnimating = false
}
}, label: {
Text("show animation")
})
}
.loadingAnimation(isAnimating: $isAnimating)
}
}
Note: I am fairly certain the issue is with View Modifier since if I call ArcsAnimationView as a regular view in AnimationView, it works as expected.
I get there to see some implementation, but I think others would prefer a simple base to start from.
here my 2 cents to show how to write an AnimatableModifier that can be used on multiple objects cleaning up ".animation" in code.
struct ContentView: View {
#State private var hideWhilelUpdating = false
var body: some View {
Image(systemName: "tshirt.fill")
.modifier(SmoothHideAndShow(hide: hideWhilelUpdating))
Text("Some contents to show...")
.modifier(SmoothHideAndShow(hide: hideWhilelUpdating))
Button( "hide and show smootly") {
hideWhilelUpdating.toggle()
}
.padding(60)
}
}
struct SmoothHideAndShow: AnimatableModifier {
var hide: Bool
var animatableData: CGFloat {
get { CGFloat(hide ? 0 : 1) }
set { hide = newValue == 0 }
}
func body(content: Content) -> some View {
content
.opacity(hide ? 0.2 : 1)
.animation(.easeIn(duration: 1), value: hide)
}
}
when pressing button, our bool will trigger animation that fades in and out our text.
I use it during network calls (omitted for clarity... and replaced with button) to hide values under remote update. When network returns, I toggle boolean.

SwiftUI TabView back to tab's root view by pressing the Tab Item

When I go deeper in a view ā€“ by using navigationLinks ā€“, I can't go back to the root view of the application by tapping on the TabItem, so if I tap on "ALL Cars" tab and click tap on "Home" again it will show the last state of that view.
How can I reset the view to show the initial state?
TabView(selection: $selectedTab) {
HomeView()
.tabItem {
if(selectedTab == 0) {
Image("home_active")
.frame(height: 28)
} else {
Image("home_inactive")
.frame(height: 28)
}
Text("HOME")
}.tag(0)
NavigationView {
CarsView()
}
.navigationBarHidden(true)
.tabItem {
if(selectedTab == 1) {
Image("cars_active")
} else {
Image("cars_inactive")
}
Text("ALL Cars").foregroundColor(.white)
}.tag(1)
}

SwiftUI - Pulsating Animation & Change Colour

I'm trying to change the colour of my animation based on the state of something. The colour change works but it animates the previous (orange) colour with it. I can't quite work out why both colours are showing. Any ideas?
struct PulsatingView: View {
var state = 1
func colourToShow() -> Color {
switch state {
case 0:
return Color.red
case 1:
return Color.orange
case 2:
return Color.green
default:
return Color.orange
}
}
#State var animate = false
var body: some View {
VStack {
ZStack {
Circle().fill(colourToShow().opacity(0.25)).frame(width: 40, height: 40).scaleEffect(self.animate ? 1 : 0)
Circle().fill(colourToShow().opacity(0.35)).frame(width: 30, height: 30).scaleEffect(self.animate ? 1 : 0)
Circle().fill(colourToShow().opacity(0.45)).frame(width: 15, height: 15).scaleEffect(self.animate ? 1 : 0)
Circle().fill(colourToShow()).frame(width: 6.25, height: 6.25)
}
.onAppear { self.animate.toggle() }
.animation(Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true))
}
}
}
You change the color, but animating view, which is set up forever is not changed - it remains as set and continues as specified - forever. So it needs to re-set the animation.
Please find below a working full module demo code (tested with Xcode 11.2 / iOS 13.2). The idea is to use ObservableObject view model as it allows and refresh view and perform some actions on receive. So receiving color changes it is possible to reset and view color and animation.
Updated for Xcode 13.3 / iOS 15.4
Main part is:
}
.onAppear { self.animate = true }
.animation(animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : .default, value: animate)
.onChange(of: viewModel.colorIndex) { _ in
self.animate = false // << here !!
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animate = true // << here !!
}
}
Complete findings and code is here

Animations triggered by events in SwiftUI

SwiftUI animations are typically driven by state, which is great, but sometimes you really want to trigger a temporary (often reversible) animation in response to some event. For example, I want to temporarily increase the size of a button when a it is tapped (both the increase and decrease in size should happen as a single animation when the button is released), but I haven't been able to figure this out.
It can sort of be hacked together with transitions I think, but not very nicely. Also, if I make an animation that uses autoreverse, it will increase the size, decrease it and then jump back to the increased state.
That is something I have been into as well.
So far my solution depends on applying GeometryEffect modifier and misusing the fact that its method effectValue is called continuously during some animation. So the desired effect is actually a transformation of interpolated values from 0..1 that has the main effect in 0.5 and no effect at 0 or 1
It works great, it is applicable to all views not just buttons, no need to depend on touch events or button styles, but still sort of seems to me as a hack.
Example with random rotation and scale effect:
Code sample:
struct ButtonEffect: GeometryEffect {
var offset: Double // 0...1
var animatableData: Double {
get { offset }
set { offset = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let effectValue = abs(sin(offset*Double.pi))
let scaleFactor = 1+0.2*effectValue
let affineTransform = CGAffineTransform(rotationAngle: CGFloat(effectValue)).translatedBy(x: -size.width/2, y: -size.height/2).scaledBy(x: CGFloat(scaleFactor), y: CGFloat(scaleFactor))
return ProjectionTransform(affineTransform)
}
}
struct ButtonActionView: View {
#State var animOffset: Double = 0
var body: some View {
Button(action:{
withAnimation(.spring()) {
self.animOffset += 1
}
})
{
Text("Press ME")
.padding()
}
.background(Color.yellow)
.modifier(ButtonEffect(offset: animOffset))
}
}
You can use a #State variable tied to a longPressAction():
Code updated for Beta 5:
struct ContentView: View {
var body: some View {
HStack {
Spacer()
MyButton(label: "Button 1")
Spacer()
MyButton(label: "Button 2")
Spacer()
MyButton(label: "Button 3")
Spacer()
}
}
}
struct MyButton: View {
let label: String
#State private var pressed = false
var body: some View {
return Text(label)
.font(.title)
.foregroundColor(.white)
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(.green))
.scaleEffect(self.pressed ? 1.2 : 1.0)
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
withAnimation(.easeInOut(duration: 0.2)) {
self.pressed = pressing
}
}, perform: { })
}
}
I believe this is what you're after. (this is how I solved this problem)
Based on dfd's link in i came up with this, which is not dependent on any #State variable. You simply just implement your own button style.
No need for Timers, #Binding, #State or other complex workarounds.
import SwiftUI
struct MyCustomPressButton: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(10)
.cornerRadius(10)
.scaleEffect(configuration.isPressed ? 0.8 : 1.0)
}
}
struct Play: View {
var body: some View {
Button("Tap") {
}.buttonStyle(MyCustomPressButton())
.animation(.easeIn(duration: 0.2))
}
}
struct Play_Previews: PreviewProvider {
static var previews: some View {
Play()
}
}
There is no getting around the need to update via state in SwiftUI. You need to have some property that is only true for a short time that then toggles back.
The following animates from small to large and back.
struct ViewPlayground: View {
#State var enlargeIt = false
var body: some View {
Button("Event!") {
withAnimation {
self.enlargeIt = true
}
}
.background(Momentary(doIt: self.$enlargeIt))
.scaleEffect(self.enlargeIt ? 2.0 : 1.0)
}
}
struct Momentary: View {
#Binding var doIt: Bool
var delay: TimeInterval = 0.35
var body: some View {
Group {
if self.doIt {
ZStack { Spacer() }
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
withAnimation {
self.doIt = false
}
}
}
}
}
}
}
Unfortunately delay was necessary to get the animation to occur when setting self.enlargeIt = true. Without that it only animates back down. Not sure if that's a bug in Beta 4 or not.

Resources