This is a basic hero animation setup, and seems to work 90% of the time. But randomly the animations start to not animate correctly and occasionally causes the app to crash. I made this as simple as possible to try and narrow down the issue. After hours of testing on device and simulator I cannot figure out why the animations sometimes does not animate correctly for some items in the grid, and cannot figure out the causes of the crash.
No error codes are thrown, except "Multiple inserted views in matched geometry group have isSource: true, results are undefined."
But if I set the source the animation will also break, without source set the animation plays correctly.. until it doesn't.
It takes a bunch of clicking on grid items, until eventually you will see the animation not fly in correctly or even appear at all, try a different grid item and it works correctly, go back to the incorrect item and it may or may not fly in correctly, keep going and eventually it will crash.
Im wondering if I'm not understanding something about SwiftUI, or this is a bug?
The grid:
struct Prototype_RecipeGrid: View {
#Namespace var namespace
#State private var selectedRecipe: Int? = nil
var body: some View {
ZStack {
NavigationView {
ScrollView {
LazyVGrid(columns: [GridItem()], spacing: 20) {
ForEach(0 ..< 8) { index in
Prototype_RecipeCard(index: index, namespace: namespace)
.onTapGesture { open(index) }
}
}
.padding(.horizontal)
}
.navigationTitle("Recipes")
}
.zIndex(1)
if selectedRecipe != nil {
Prototype_RecipeDetail(close: close, index: selectedRecipe!, namespace: namespace)
.onTapGesture { close() }
.zIndex(3)
}
}
}
private func open(_ index: Int) {
withAnimation(.spring()) {
selectedRecipe = index
}
}
private func close() {
withAnimation(.linear(duration: 0.25)) {
selectedRecipe = nil
}
}
}
The grid cards:
struct Prototype_RecipeCard: View {
let index: Int
let namespace: Namespace.ID
var body: some View {
Color.clear.overlay(
GeometryReader { geo in
ZStack(alignment: .topLeading) {
Image("047224BD-6F84-4990-9E4D-B46E425FBC5D")
.resizable()
.aspectRatio(contentMode: .fill)
.matchedGeometryEffect(id: "img\(index)", in: namespace)
.frame(height: 180)
.frame(width: geo.size.width)
Prototype_RecipeTitle()
.matchedGeometryEffect(id: "title\(index)", in: namespace)
}
}
)
.matchedGeometryEffect(id: "detail\(index)", in: namespace)
.clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
//.transition(.move(edge: .bottom))
.shadow(radius: 10)
.frame(height: 180)
}
}
The hero zoomed card:
struct Prototype_RecipeDetail: View {
var close: () -> Void
let index: Int
let namespace: Namespace.ID
var body: some View {
Color.clear.overlay(
GeometryReader { geo in
ScrollView(showsIndicators: false) {
VStack {
ZStack(alignment: .bottomLeading) {
Image("047224BD-6F84-4990-9E4D-B46E425FBC5D")
.resizable()
.aspectRatio(contentMode: .fill)
.matchedGeometryEffect(id: "img\(index)", in: namespace)
.frame(height: 375)
.frame(width: geo.size.width)
Prototype_RecipeTitle()
.matchedGeometryEffect(id: "title\(index)", in: namespace)
}
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed risus sollicitudin, placerat massa non, facilisis tellus. Ut lorem est, sodales ut diam in, imperdiet tempor mauris. Donec sit amet elit dapibus, scelerisque tellus a, malesuada eros. Phasellus rutrum, turpis vel tempor placerat, nulla est blandit justo, sit amet faucibus libero nibh eu nisl. Curabitur quis mauris purus. Maecenas at laoreet tortor. Duis pretium auctor pulvinar. Nulla posuere risus nec sapien ornare sagittis. Nunc molestie aliquam blandit. Curabitur ut elementum felis, sit amet bibendum justo. Integer elementum et justo a bibendum. Vestibulum bibendum purus eu arcu scelerisque tempus. Donec interdum auctor neque vel tincidunt. Nulla facilisi. Quisque ut semper felis, vitae vestibulum ante. Ut et dictum tellus, ultricies condimentum orci. Aenean gravida massa vitae massa finibus suscipit. Duis sit amet ex facilisis, viverra purus non, gravida ante. Ut urna dui, consectetur ac tempus et, accumsan non est. Suspendisse sed volutpat metus. Maecenas ac neque nisl. In a efficitur purus. Curabitur ex est, vehicula vitae ipsum pulvinar, aliquet elementum purus. Mauris rhoncus maximus leo, cursus sodales elit interdum vitae. Ut et felis et purus pulvinar efficitur. Nullam pellentesque molestie nisi at rhoncus. Vestibulum sed urna non erat aliquet volutpat vel sit amet quam. Ut neque lorem, auctor a ipsum eget, volutpat consectetur urna. Phasellus eleifend maximus libero, et maximus velit lobortis non.")
.padding(.horizontal)
Spacer()
}
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
.onTapGesture { close() }
}
}
)
.clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
.matchedGeometryEffect(id: "detail\(index)", in: namespace)
//.transition(.move(edge: .bottom))
//.frame(height: 750)
.edgesIgnoringSafeArea(.all)
}
}
Related
Is it possible to fade in/out text at top and bottom of scrollview as users scrolls text? Xcode 14.2, iOS 16, Swift 5.7
I have research other solutions such as this: SwiftUI - fade out a ScrollView
I have tried the following, but it is fading out the side horizontally and adding color. I want it to be vertical and transparent fade as in the picture.
ScrollView {
ScrollViewReader { scrollViewProxy in
VStack {
ForEach(chatMessages, id: \.id) { message in
messageView(message: message)
}
.mask(
VStack(spacing: 0) {
// Top gradient
LinearGradient(gradient:
Gradient(
colors: [Color.black.opacity(0), Color.black]),
startPoint: .leading, endPoint: .trailing
)
.frame(width: 250)
// Middle
Rectangle().fill(Color.black)
// Bottom gradient
LinearGradient(gradient:
Gradient(
colors: [Color.black, Color.black.opacity(0)]),
startPoint: .leading, endPoint: .trailing
)
.frame(width: 250)
}
)
}
}
}
I have used this code to produce a horizontal fade, but need vertical on both top and bottom. Here is what is in simulator now.
See image:
This can be achieved using a Rectangle() mask with the top and bottom "removed" using a .blendMode(.destinationOut)
This allows you to have a fixed height gradient at the top and bottom.
struct ContentView: View {
#State var nameTextField: String = ""
var body: some View {
ScrollView {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\n").bold()
}
.mask {
Rectangle()
.overlay(alignment: .top) {
ScrollMask(isTop: true)
}
.overlay(alignment: .bottom) {
ScrollMask(isTop: false)
}
}
.padding()
.foregroundColor(.white)
.scrollContentBackground(.hidden)
.background {
Image("wave")
}
}
}
struct ScrollMask: View {
let isTop: Bool
var body: some View {
LinearGradient(colors: [.black, .clear], startPoint: UnitPoint(x: 0.5, y: isTop ? 0 : 1), endPoint: UnitPoint(x: 0.5, y: isTop ? 1 : 0))
.frame(height: 150)
.frame(maxWidth: .infinity)
.blendMode(.destinationOut)
}
}
An alternative way would be to use a single LinearGradient with multiple Colors/Stops. In this cases the top third fades in, and the bottom third fades out:
.mask {
LinearGradient(colors: [.clear, .black, .black, .clear],
startPoint: UnitPoint(x: 0.5, y: 0), endPoint: UnitPoint(x: 0.5, y: 1))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
See here for more info on reverse masks
I have some UITextView which I want to place inside a VStack. I want them to display one directly under the other and completely expanded, without having to scroll inside the TextView.
struct TextView: UIViewRepresentable {
let text : String
let titulo : String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.isEditable = false
textView.isSelectable = false
textView.bounces = false
DispatchQueue.main.async {//Needs to execute in another thread, otherwise it crashes (Swift bug)
textView.attributedText = text.htmlToAttributedString(size: 16, titulo: titulo)
}
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
My view looks like:
struct PageView: View {
let sampleText = """
<h1>Lorem ipsum dolor sit amet,</h1>
<h2>consectetur adipiscing elit,</h2>
<h3> sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.<h3>
<p> Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p><br>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.<br>
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"""
var body: some View{
TabView{
HStack{
VStack{
//Views that take up half of the screen
}
VStack(alignment: .leading){
ScrollView{
TextView(text: sampleText, titulo: "")
TextView(text: sampleText, titulo: "")
Spacer()
}
}
}
}.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
And my htmlToAttributedString function:
func htmlToAttributedString(size: CGFloat, titulo: String) -> NSAttributedString? {
var attributedString = NSAttributedString()
let texto = self.replacingOccurrences(of: "\n", with: "<br>")
if(!self.isEmpty)
{
var htmlTemplate = """
<!doctype html>
<html>
<head>
<style>
body {
font-family: -apple-system;
font-size: \(size)px;
}
</style>
</head>
<body>
"""
if(titulo != "")
{
htmlTemplate += "<h3>\(titulo)</h3>"
}
htmlTemplate += """
\(texto)
</body>
</html>
"""
guard let data = htmlTemplate.data(using: .utf8) else { return NSAttributedString(string: "No se han obtenido los datos correctamente")}
do {
attributedString = try NSAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue,
],
documentAttributes: nil)
} catch {}
}
return attributedString
}
}
I've already tried scaledToFit, maxHeight = .infinity and fixedSize.
Well, the view doesn't display just because the ScrollView, eliminate the ScrollView to the following code:
var body: some View{
TabView{
HStack{
VStack{
//Views that take up half of the screen
Image(systemName: "plus")
}
TextView(text: sampleText, titulo: "")
TextView(text: sampleText, titulo: "")
}
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
Maybe this will be the result that you want.
I'm trying to add an expand/colapse animation on a Text with multiple lines, and I'm having a strange behaviour.
Below is a gif with the issue. I've set slow animations to make it clear.
https://www.dropbox.com/s/sx41g9tfx4hd378/expand-collapse-stack_overflow.gif
I'm animating the height property of the view, and it seems that the Text will convert immediately to one line disregarding the animation period. Here's some code:
struct ContentView: View {
#State var expanded = false
var body: some View {
VStack(spacing: 20) {
HStack {
Button(expanded ? "Colapse" : "Expand") {
withAnimation {
self.expanded.toggle()
}
}
}
VStack(spacing: 10) {
Text(bigText)
Text(bigText)
}
.frame(height: expanded ? .none : 0)
.clipped()
.background(Color.red)
Text("Thist is another text underneath the huge one. ")
.font(.system(.headline))
.foregroundColor(.red)
Spacer()
}
}
}
I have tried a lot of other things, and this is currently the closest to the desired output, which is the same as animating a label inside a UIStackView in UIKit.
Is there a way to do this properly? Is this a bug?
Normally the problem is from the developer, but I noticed that if I use a DisclosureGroup the animation works when it's expanding, but when it's collapsing it simply has no animation. So this might actually be a limitation of multi line Text?
Thank you very much.
Well, the problem is that we animate frame between nil and 0, but to internal Text items only edge values are transferred.
To solve this it should be done two steps:
make height animatable data, so every change to height value passed to content
calculate real max height of text content, because we need concrete values of animatable range.
So here is a demo of approach. Prepared with Xcode 13 / iOS 15
Note: slow animation is activated in Simulator for better visibility
let bigText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"""
struct ContentView: View {
// we need `true` on construction (not visible) to calculate
// max content height
#State var expanded = true // << required initial true !!
#State private var maxHeight: CGFloat?
var body: some View {
VStack(spacing: 20) {
HStack {
Button(expanded ? "Colapse" : "Expand") {
withAnimation {
self.expanded.toggle()
}
}
}
VStack(spacing: 10) {
Text(bigText)
Text(bigText)
}
.background(GeometryReader { // read content height
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
.onPreferenceChange(ViewHeightKey.self) {
if nil == self.maxHeight {
self.maxHeight = $0 // << needed once !!
}
}
.modifier(AnimatingFrameHeight(height: expanded ? maxHeight ?? .infinity : 0))
.clipped()
.background(Color.red)
Text("Thist is another text underneath the huge one. ")
.font(.system(.headline))
.foregroundColor(.red)
Spacer()
}
.onAppear {
// this cases instance redraw on first render
// so initial state will be invisible for user
expanded = false // << set if needed here !!
}
}
}
struct AnimatingFrameHeight: AnimatableModifier {
var height: CGFloat = 0
var animatableData: CGFloat {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
So I am implementing a UI in SwiftUI and having trouble implementing the little "title tab" all the way to the left in the picture below. Basically I have a title that is rotated 90 degrees to display on the side of the tab and I want the user to be able to enter a custom title so I need the title area to be able to dynamically resize. However I also have it embeded in an HStack and only want it taking a small amount of the space, rather than a full third. When I implement layoutPriority it decreases the horizontal space that the title area takes, but it no longer expands vertically if the title text takes up more space than the other elements in the HStack. If I remove the layoutPriority it expands vertically to display the full title text as I want but also takes up a full third of the HStack which I dont want. Is there a way I am missing to implement this?
UIElement
HStack{
EventTitleBackground(name:name).rotationEffect(.degrees(270))
.frame(minHeight: 0, maxHeight: .infinity)
.frame(minWidth: 0, maxWidth: .infinity)
.layoutPriority(2)
Spacer()
VStack(alignment: .leading){
Text(time)
.font(.title)
Spacer()
Text("\(truncatedLatitude) \(truncatedLongitude)")
.font(.title)
Spacer()
Text("Altitude: \(truncatedAltitude)")
.font(.title)
}
.layoutPriority(4)
Spacer()
VStack(alignment: .leading){
HStack{
Text("BOBR: \(bobrLargeText)")
.font(.title)
Text(" \(bobrSmallText)")
.font(.body)
}
Spacer()
Text("Heading | Course: \(heading) | \(heading)")
.font(.title)
Spacer()
Text("Groundspeed: \(groundSpeed)")
.font(.title)
}
.layoutPriority(4)
Spacer()
}
I suggest you to check this simple example. By tap on rectangle you can rotate it and with sliders you can change the "width" of rectangles. To be honest, it works exactly as mentioned in Apple docs.
import SwiftUI
struct ContentView: View {
#State var angle0 = Angle(degrees: 0)
#State var angle1 = Angle(degrees: 0)
#State var width0: CGFloat = 100
#State var width1: CGFloat = 100
var body: some View {
VStack {
HStack {
Color.red.frame(width: width0, height: 50)
.onTapGesture {
self.angle0 += .degrees(90)
}
.rotationEffect(angle0).border(Color.blue)
Color.green.frame(width: width1, height: 50)
.onTapGesture {
self.angle1 += .degrees(90)
}
.rotationEffect(angle1).border(Color.blue)
Spacer()
}
Slider(value: $width0, in: 50 ... 200) {
Text("red")
}
Slider(value: $width1, in: 50 ... 200) {
Text("green")
}
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
the result is probably far away from what did you expect ...
Let change the red color part
Color.red//.frame(width: width0, height: 50)
.onTapGesture {
self.angle0 += .degrees(90)
}
.rotationEffect(angle0).border(Color.blue).frame(width: 50, height: width0)
and see the difference
Solution for you could be something like
import SwiftUI
struct VerticalText: View {
let text: String
#Binding var size: CGSize
var body: some View {
Color.red.overlay(
Text(text).padding().fixedSize()
.background(
GeometryReader { proxy -> Color in
// avoid layout cycling!!!
DispatchQueue.main.async {
self.size = proxy.size
}
return Color.clear
}
).rotationEffect(.degrees(-90))
)
.frame(width: size.height, height: size.width)
.border(Color.green)
}
}
struct ContentView: View {
#State var size: CGSize = .zero
var body: some View {
HStack(alignment: .top) {
VerticalText(text: "Hello, World!", size: $size)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus semper eros non condimentum mattis. In hac habitasse platea dictumst. Mauris aliquam, enim eu vehicula sodales, odio enim faucibus eros, scelerisque interdum libero mi id elit. Donec auctor ipsum at dolor pellentesque, sed dapibus felis dignissim. Sed ac euismod purus, sed sollicitudin leo. Maecenas ipsum felis, ultrices a urna nec, dapibus viverra libero. Pellentesque quis est nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum luctus a est eget posuere.")
}.frame(height: size.width)
.border(Color.red).padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
See, that the size of VerticalText is stored in the parent, doesn't matter if you use it or not. Otherwise the parent will not layout properly.
Since List doesn't look like its configurable to remove the row dividers at the moment, I'm using a ScrollView with a VStack inside it to create a vertical layout of text elements. Example below:
ScrollView {
VStack {
// ...
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer mattis ullamcorper tortor, nec finibus sapien imperdiet non. Duis tristique eros eget ex consectetur laoreet.")
.lineLimit(0)
}.frame(width: UIScreen.main.bounds.width)
}
The resulting Text rendered is truncated single-line. Outside of a ScrollView it renders as multi-line. How would I achieve this inside a ScrollView other than explicitly setting a height for the Text frame ?
In Xcode 11 GM:
For any Text view in a stack nested in a scrollview, use the .fixedSize(horizontal: false, vertical: true) workaround:
ScrollView {
VStack {
Text(someString)
.fixedSize(horizontal: false, vertical: true)
}
}
This also works if there are multiple multiline texts:
ScrollView {
VStack {
Text(someString)
.fixedSize(horizontal: false, vertical: true)
Text(anotherLongString)
.fixedSize(horizontal: false, vertical: true)
}
}
If the contents of your stack are dynamic, the same solution works:
ScrollView {
VStack {
// Place a single empty / "" at the top of your stack.
// It will consume no vertical space.
Text("")
.fixedSize(horizontal: false, vertical: true)
ForEach(someArray) { someString in
Text(someString)
.fixedSize(horizontal: false, vertical: true)
}
}
}
You can force views to fill their ideal size, for example in a vertical ScrollView:
ScrollView {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer mattis ullamcorper tortor, nec finibus sapien imperdiet non. Duis tristique eros eget ex consectetur laoreet.")
.fixedSize(horizontal: false, vertical: true)
}
Feels a little better to me than modifying the frame.
It seems like there is bug in SwiftUI. For now you have to specify height for your VStack container
ScrollView {
VStack {
// ...
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer mattis ullamcorper tortor, nec finibus sapien imperdiet non. Duis tristique eros eget ex consectetur laoreet.")
.lineLimit(nil)
}.frame(width: UIScreen.main.bounds.width, height: 500)
}
The following works for me with Beta 3 - no spacer, no width constraint, flexible height constraint 👍:
ScrollView {
VStack {
Text(longText)
.lineLimit(nil)
.font(.largeTitle)
.frame(idealHeight: .infinity)
}
}
The correct solution is to just make sure to set the alignment for your stack:
VStack(alignment: .leading)
ScrollView {
VStack(alignment: .leading) {
// ...
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer mattis ullamcorper tortor, nec finibus sapien imperdiet non. Duis tristique eros eget ex consectetur laoreet.")
.lineLimit(0)
}.frame(width: UIScreen.main.bounds.width)
}
This way you don't need the fixedSize as the layout is properly defined.