This code shows the general behavior I'm trying to achieve, but how can I make this continuous so there's no apparent end to the image, with no gaps of white space and a smooth transition? AND - isn't there a better way to do this???
Thanks!
import SwiftUI
struct ContentView: View {
#State private var xVal: CGFloat = 0.0
#State private var timer = Timer.publish(every: 0.05, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Image("game_background_2") //image attached
.aspectRatio(contentMode: .fit)
.offset(x: xVal, y: 0)
.transition(.slide)
.padding()
.onReceive(timer) {_ in
xVal += 2
if xVal == 800 { xVal = 0 }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here's a simple approach that could work. First use a GeometryReader to set the frame of the screen. Then add an HStack with 2 images inside. The first image is full size and the 2nd image is limited to the width of the screen. Finally, animate the HStack to move from the .trailing to the .leading edge of the screen frame. We set the animation .repeatForever so that it continues loops and autoReverses: false so that it restarts. When it restarts, however, it should be seamless because the restart position is identical to the 2nd image.
struct ImageBackgroundView: View {
#State var animate: Bool = false
let animation: Animation = Animation.linear(duration: 10.0).repeatForever(autoreverses: false)
var body: some View {
GeometryReader { geo in
HStack(spacing: -1) {
Image("game_background_2")
.aspectRatio(contentMode: .fit)
Image("game_background_2")
.aspectRatio(contentMode: .fit)
.frame(width: geo.size.width, alignment: .leading)
}
.frame(width: geo.size.width, height: geo.size.height,
alignment: animate ? .trailing : .leading)
}
.ignoresSafeArea()
.onAppear {
withAnimation(animation) {
animate.toggle()
}
}
}
}
Related
It seems that Transition.slide and Transition.move animations are broken. Using AnyTransition.slide.animation() does not animate. It requires an additional .animation() modifier. This is a problem, since often I want to animate only the slide and nothing else.
Here is a demo. The goal is to have a sliding animation without animating the black ball. The .scale and .opacity animations work just fine. But .slide and .move either do not animate, or they animate the black ball.
import SwiftUI
struct ContentView: View {
#State var screens: [Color] = [Color.red]
var body: some View {
ZStack {
ForEach(screens.indices, id: \.self) { i -> AnyView in
let screen = screens[i]
return AnyView(
Screen(color: screen)
.zIndex(Double(i))
// Try uncommenting these lines one by one. The scale and opacity animations work fine. But move and slide animate only when the additional .animation modifier is uncommented below.
// This is a problem, since uncommenting the standalone .animation modifier, also animates everything inside the Screen() view, which is not what I want.
// .transition(AnyTransition.scale.animation(.linear(duration: 2)))
// .transition(AnyTransition.opacity.animation(.linear(duration: 2)))
// .transition(AnyTransition.move(edge: .leading).animation(.linear(duration: 2)))
.transition(AnyTransition.slide.animation(.linear(duration: 2)))
// .animation(.linear(duration: 2))
)
}
VStack(spacing: 50) {
Text("Screens Count: \(screens.count)")
Button("prev") {
removeScreen()
}
.padding()
.background(Color.white)
Button("next") {
addScreen()
}
.padding()
.background(Color.white)
}
.zIndex(1000)
}
}
func addScreen() {
let colors = [Color.red, Color.blue, Color.green, Color.yellow]
screens.append(colors[screens.count % colors.count])
}
func removeScreen() {
guard screens.count > 1 else { return }
screens.removeLast()
}
}
struct Screen: View {
static var width = UIScreen.main.bounds.width / 2 - 25
static var height = UIScreen.main.bounds.height / 2 - 25
var color: Color
#State var offset = CGSize(width: Self.width, height: -1 * Self.height)
var body: some View {
ZStack {
color
.frame(maxWidth: .infinity, maxHeight: .infinity)
Circle()
.frame(width: 50)
.offset(offset)
}
.onAppear { offset = CGSize(width: Self.width, height: Self.height) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any thoughts how to use slide and move animations without animating everything else inside the view?
Ok, I have a solution. It's not perfect, but it's workable. Putting a view inside a NavigationView somehow separates .animation modifiers:
struct Screen: View {
static var width = UIScreen.main.bounds.width / 2 - 25
static var height = UIScreen.main.bounds.height / 2 - 25
var color: Color
#State var offset = CGSize(width: Self.width, height: -1 * Self.height)
var body: some View {
NavigationView {
ZStack {
color
.frame(maxWidth: .infinity, maxHeight: .infinity)
Circle()
.frame(width: 50)
.offset(offset)
}
.onAppear { offset = CGSize(width: Self.width, height: Self.height) }
.animation(.none)
.navigationBarHidden(true)
}
}
}
I try to make a smooth animation when the NetStatus change but it's not working like i want.
I want to get the same effect as when i press the button with the toggle animation. The commented button animation is working great and i try to replicate it with the scaling of the height of the text frame.
The commented button code is just for a working example of the animation effect that i want (expand and close gracefully), i don't need this code.
How can i do that?
import SwiftUI
struct NoNetwork: View {
let screenSize: CGRect = UIScreen.main.bounds
#ObservedObject var online = NetStatus()
var body: some View {
VStack{
Text("NoNetworkTitle")
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: screenSize.width, height: self.online.connected ? 0 : 40, alignment: .center)
// .animation(.easeIn(duration: 5))
.background(Color.red)
// Button(action: {
// withAnimation {
// self.online.connected.toggle()
// }
// }, label: {
// Text("Animate")
// })
}
}
}
struct NoNetwork_Previews: PreviewProvider {
static var previews: some View {
NoNetwork()
}
}
To animate when online.connected changes, put the .animation modifier on the VStack:
VStack{
Text("NoNetworkTitle")
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: screenSize.width, height: self.online.connected ? 0 : 40, alignment: .center)
.background(Color.red)
Button(action: {
self.online.connected.toggle()
}, label: {
Text("Animate")
})
}
.animation(.easeInOut(duration: 0.5))
This will animate the other views in the VStack as the Text appears and disappears.
I'm making a WatchOS app that displays a bunch of real-time arrival times. I want to place a view, a real-time indicator I designed, on the trailing end of each cell of a List that will be continuously animated.
The real-time indicator view just has two image whose opacity I'm continuously animating. This View by itself seems to work fine:
animated view by itself
However, when embedded inside a List then inside an HStack the animation seems to be affecting the position of my animated view not only its opacity.
animated view inside a cell
The distance this view travels seems to only be affected by the height of the HStack.
Animated view code:
struct RTIndicator: View {
#State var isAnimating = true
private var repeatingAnimation: Animation {
Animation
.spring()
.repeatForever()
}
private var delayedRepeatingAnimation: Animation {
Animation
.spring()
.repeatForever()
.delay(0.2)
}
var body: some View {
ZStack {
Image("rt-inner")
.opacity(isAnimating ? 0.2 : 1)
.animation(repeatingAnimation)
Image("rt-outer")
.opacity(isAnimating ? 0.2 : 1)
.animation(delayedRepeatingAnimation)
}
.frame(width: 16, height: 16, alignment: .center)
.colorMultiply(.red)
.padding(.top, -6)
.padding(.trailing, -12)
.onAppear {
self.isAnimating.toggle()
}
}
}
All code:
struct SwiftUIView: View {
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
RTIndicator()
}.padding(8)
}
}
}
Here is found workaround. Tested with Xcode 12.
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
}
.overlay(RTIndicator(), alignment: .trailing) // << here !!
.padding(8)
}
}
Although it's pretty hacky I have found a temporary solution to this problem. It's based on the answer from Asperi.
I have create a separate View called ClearView which has an animation but does not render anything visual and used it as a second overall in the same HStack.
struct ClearView: View {
#State var isAnimating = false
var body: some View {
Rectangle()
.foregroundColor(.clear)
.onAppear {
withAnimation(Animation.linear(duration: 0)) {
self.isAnimating = true
}
}
}
}
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
}
.overlay(RTIndicator(), alignment: .trailing)
.overlay(ClearView(), alignment: .trailing)
.padding(8)
}
}
I have a List made of cells, each containing an image, and a column of text, which I wish laid out in a specific way. Image on the left, taking up a quarter of the width. The rest of the space given to the text, which is left-aligned.
Here's the code I got:
struct TestCell: View {
let model: ModelStruct
var body: some View {
HStack {
Image("flag")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.size.width * 0.25)
VStack(alignment: .leading, spacing: 5) {
Text("Country: Moldova")
Text("Capital: Chișinău")
Text("Currency: Leu")
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
}
}
struct TestCell_Previews: PreviewProvider {
static var previews: some View {
TestCell()
.previewLayout(.sizeThatFits)
.previewDevice("iPhone 11")
}
}
And here are 2 examples:
As you can see, the height of the whole cell varies based on the aspect ratio of the image.
$1M question - How can we make the cell height hug the text (like in the second image) and not vary, but rather shrink the image in a scaleAspectFit manner inside the allocated rectangle
Note!
The text's height can vary, so no hardcoding.
Couldn't make it work with PreferenceKeys, as the cells will be part of a List, and there's some peculiar behaviour I'm trying to grasp around cell reusage, and onPreferenceChange not being called when 2 consecutive cells have the same height. To exhibit all this combined behaviour, make sure your model varies between cells when you test it.
Here is a possible solution, however it uses GeometryReader inside the background property of the VStack, to detect their height. That height is being applied to the Image then. I used SizePreferenceKey from this solution.
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue()
}
}
struct ContentView6: View {
#State var childSize: CGSize = .zero
var body: some View {
HStack {
Image("image1")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.size.width * 0.25, height: self.childSize.height)
VStack(alignment: .leading, spacing: 5) {
Text("Country: Moldova")
Text("Capital: Chișinău")
Text("Currency: Leu")
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.background(
GeometryReader { proxy in
Color.clear.preference(key: SizePreferenceKey.self, value: proxy.size)
}
)
}
.onPreferenceChange(SizePreferenceKey.self) { preferences in
self.childSize = preferences
}
.border(Color.yellow)
}
}
Will look like this.. you can apply different aspect ratios for the Image of course.
This is what worked for me to constrain a color view to the height of text content in a cell:
A height reader view:
struct HeightReader: View {
#Binding var height: CGFloat
var body: some View {
GeometryReader { proxy -> Color in
update(with: proxy.size.height)
return Color.clear
}
}
private func update(with value: CGFloat) {
guard value != height else { return }
DispatchQueue.main.async {
height = value
}
}
}
You can then use the reader in a compound view as a background on the view you wish to constrain to, using a state object to update the frame of the view you wish to constrain:
struct CompoundView: View {
#State var height: CGFloat = 0
var body: some View {
HStack(alignment: .top) {
Color.red
.frame(width: 2, height: height)
VStack(alignment: .leading) {
Text("Some text")
Text("Some more text")
}
.background(HeightReader(height: $height))
}
}
}
struct CompoundView_Previews: PreviewProvider {
static var previews: some View {
CompoundView()
}
}
I have found that using DispatchQueue to update the binding is important.
Goal:
1- Shows a view (Blue) that covers entire screen
2- When tapped on bottom (top right corner), it shows an HStack animating right side HStack (Green) "Slide Offset animation".
import SwiftUI
struct ContentView: View {
#State var showgreen = false
var body: some View {
NavigationView {
HStack {
Rectangle()
.foregroundColor(.blue)
if showgreen {
Rectangle()
.foregroundColor(.green)
.offset(x: showgreen ? 0 : UIScreen.main.bounds.width)
.animation(.easeInOut)
}
}
.navigationBarItems(trailing:
Button(action: { self.showgreen.toggle() }) {
Image(systemName: "ellipsis")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
The code works, however I cannot get the Green "Slide Offset animation" to work. Really appreciate any help! : )
Instead of using the if conditional, you need the green rectangle to be already present, and just offscreen. When showgreen toggles, you need to shrink the size of the blue rectangle, which will make room for the green rectangle.
struct ContentView: View {
#State var showgreen = false
var body: some View {
NavigationView {
HStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: showgreen ? UIScreen.main.bounds.width / 2 : UIScreen.main.bounds.width)
.animation(.easeInOut)
Rectangle()
.foregroundColor(.green)
.animation(.easeInOut)
}
.navigationBarItems(trailing:
Button(action: { self.showgreen.toggle() }) {
Image(systemName: "ellipsis")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}