LazyVGrid miss handles the tap - swiftui

I am using the LazyVGrid in my app and a faced such problem
There are two columns in the grid and that is handling the tap in the wrong place
On the screenshot, red arrows - places of the click
I used to think that would open the MacBookAir's card, but that opens the iPadAir's one
struct GalleryView: View {
var ns: Namespace.ID
#EnvironmentObject private var model: Model
let columns = [
GridItem(.adaptive(minimum: 400))
]
var body: some View {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(Card.list) { card in
if card != model.selectedCard {
GalleryCardView(card: card, ns: ns)
.onTapGesture {
withAnimation(.openCard) {
model.selectedCard = card
model.isDetailShown = true
}
}
} else {
Color(.clear)
.frame(width: 400, height: 400)
}
}
}
}
}
Here is the GalleryCardView:
struct GalleryCardView: View {
let card: Card
var ns: Namespace.ID
var body: some View {
ZStack {
Image(card.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 400, height: 400)
.clipped()
.matchedGeometryEffect(id: "\(card.id)-image", in: ns)
.overlay(alignment: .bottom) {
Text(card.title)
.font(.largeTitle)
.fontWeight(.bold)
.frame(maxWidth: .infinity)
.frame(height: 50)
.padding()
.background {
Rectangle()
.fill(.ultraThinMaterial)
}
.matchedGeometryEffect(id: "\(card.id)-title", in: ns)
}
}
.clipShape(RoundedRectangle(cornerRadius: 30))
.matchedGeometryEffect(id: "\(card.id)-card", in: ns)
}
}
UPD:
I had some work around and find out that the problem is with the GalleryCardView - even though the image is clipped, it took extra space outside the frame. If I change the aspect ratio to .fit, everything works fine

Related

RoundedRectangle background colour does not crop and TextEditor transparent background

I have a messaging interface. When user types in to the texteditor it will be append to messagesDBArray and will be displayed in textview. Once new messages are there it should scroll to the bottom. But I'm having issues.
Errors: no errors
RoundedRectangle background colour green overflows from corners (does not crop as rounded)
TextEditor (not textview) is not transparent (so it can have rounded rectangle color underneath)
proxy.scrollTo(id, anchor: .bottom) does not scrolls to the last message.
import SwiftUI
final class ViewModel: ObservableObject {
#Published var messagesDBArray : [SingleMessageBubbleModel] = []
}
struct SingleMessageBubbleModel: Identifiable {
let id = UUID()
var text: String
var received: Bool
var timeStamp: Date
}
var messagesDBArray : [SingleMessageBubbleModel] = []
struct ContentView: View {
#ObservedObject private var messageArrayObservedObject = ViewModel()
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String = ""
var body: some View {
VStack (alignment: .center) {
ZStack (alignment: .center) {
HStack () {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.stroke(Color.brown, lineWidth: 1)
.frame(width: 300, alignment: Alignment.top )
.padding([.bottom], 5)
.clipped()
.background(Color.green)
}
HStack () {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(
messageArrayObservedObject.messagesDBArray,
id: \.id
) {
message in MessageBubble(message: message)
}
}
}
.frame(alignment: .center)
.background(Color.clear)
.padding (.vertical, 5)
.padding (.horizontal,5)
.padding(.bottom, 5)
.onChange(
of: messageArrayObservedObject.messagesDBArray.count
) { id in
// When the lastMessageId changes, scroll to the bottom of the conversation
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .center)
}
.frame(width: 295, alignment: Alignment.center )
}
HStack () {
VStack {
ZStack (alignment: .center) {
HStack () {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.stroke(Color.brown , lineWidth: 1)
.frame(width: 295, alignment: Alignment.top )
.padding([.bottom], 5)
.clipped()
.background(Color.green)
// .background(Color("#E5F2E4"))
}
HStack () {
TextEditor (text: $textTyped)
.frame(height: 200, alignment: .leading)
.padding(.horizontal, 10)
.background(.clear)
}
}
.frame(width: 290, alignment: Alignment.top )
.padding(.top, 5)
}
}
}
}
struct MessageBubble: View {
var message: SingleMessageBubbleModel
#State private var showTime = false
var body: some View {
VStack(alignment: message.received ? .leading : .trailing) {
HStack {
Text(message.text)
.padding()
.background(message.received ? Color.gray : Color.blue)
.cornerRadius(30)
}
.frame(maxWidth: 300, alignment: message.received ? .leading : .trailing)
.onTapGesture {
withAnimation {
showTime.toggle()
}
}
}
.frame(maxWidth: .infinity, alignment: message.received ? .leading : .trailing)
.padding(message.received ? .leading : .trailing)
.padding(.horizontal, 4)
}
}
for the first error you should use that code instead of your code where you make a background with RoundRectangle the same to your base rectangle and make the fill of that the color you want which is green
RoundedRectangle(cornerRadius: 25, style: .continuous)
.stroke(Color.brown, lineWidth: 1).background(RoundedRectangle(cornerRadius: 25).fill(Color.green))
.frame(width: 300, alignment: Alignment.top )
.padding([.bottom], 5)
.clipped()
the second issue in your ContentView you should init your UITextView background color to clear and after that make your textEditor Color clear using that code
init() {
UITextView.appearance().backgroundColor = .clear
}
and make your textEditor background clear
TextEditor (text: $textTyped)
.frame(height: 200, alignment: .leading)
.padding(.horizontal, 10)
.background(Color.clear)
and the third issue is that i think you are using the array count but you should use the id of each message so when if we suppose that the last message-id is 728398 in your onChange
onChange(of: messageArrayObservedObject.messagesDBArray.count) { id in
// When the lastMessageId changes, scroll to the bottom of the conversation
withAnimation {
print("ididididid\(id)")
proxy.scrollTo(messageArrayObservedObject.messagesDBArray.last, anchor: .bottom)
}
}
your are using the ( messageArrayObservedObject.messagesDBArray.count )counts of messages like 5 message so you are scrolling to 5 not to the id of message which is 728398

Scrollable content not working as expected in SwiftUI

I'm trying to create a scroll view with my custom view, but when I add scroll to view it's not working as expected, without scroll view working fine.
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
VStack {
ForEach (0..<2) { _ in
ListItem()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// But the below code is working fine.
struct ContentView: View {
var body: some View {
VStack {
ForEach (0..<2) { _ in
ListItem()
}
}
}
}
// List Item
struct ListItem: View {
var body: some View {
VStack {
HStack {
Image("steve")
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
.frame(maxWidth:44, maxHeight: 44)
VStack {
Text("Steve Jobs")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text("1 hour ago")
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
}
ZStack(alignment:.top) {
GeometryReader { geometry in
VStack {
ZStack {
Image("poster_1")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.12),
radius: 4, x: 1, y: 1)
.frame(width: geometry.size.width - 64,
height: geometry.size.height * 0.35)
.padding([.horizontal], 32)
.clipped()
ZStack {
Rectangle()
.fill(Color.black.opacity(0.75))
.frame(maxWidth:84 , maxHeight: 84)
.cornerRadius(12)
Image(systemName: "play.fill")
.font(.system(size: 44, weight: .bold))
.foregroundColor(.white)
}
}
VStack {
Text("Game of Thrones")
.accentColor(Color.gray.opacity(0.25))
.font(Font.subheadline.weight(.bold))
.padding([.horizontal], 32)
.padding([.bottom], 2)
.frame(maxWidth: .infinity,
alignment: .leading)
VStack {
Text("Game of Thrones is an American fantasy drama television series created by David Benioff and D. B. Weiss for HBO. ")
.accentColor(Color.gray.opacity(0.25))
.font(.footnote)
.frame(maxWidth: .infinity,
alignment: .leading)
.padding([.horizontal], 32)
Text("Show more...")
.accentColor(Color.gray.opacity(0.01))
.font(Font.footnote.weight(.bold))
.frame(maxWidth: .infinity,
alignment: .trailing)
.padding([.trailing], 32).onTapGesture {
print("okay")
}
}
}
}
}
}
}
}
}
ListItem contains multiple views which creates publisher info and movie information as shown below image.
Scrollview is scrolling but images are not shown in view as first image.
It is the geometry reader that you have in ListItem. Because neither a GeometryReader nor a Scrollview have their own size. Since neither no what size to render, they collapse. This is what you are seeing in your view. See this answer. The solution is to put the GeometryReader into ContentView outside the Scrollview and send the GeometryProxy that you called geometry into ListItem something like this:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
VStack {
ForEach (0..<2) { _ in
ListItem(geometry: geometry)
}
}
} // Scrollview
} // GeometryReader
}
}
struct ListItem: View {
let geometry: GeometryProxy
var body: some View {
...
}
This seems to fix it in Preview, though you may have to change your multipliers in the .frame() that uses geometry to size it how you want.

How can I hide extra sized background View in a View?

I have a simple Rectangle which I put a VStack of Numbers in background of that, you can see it in code and pic! the problem is the VStack is taller than Rectangle there for it goes outside of Rectangle, I want the extra part be hidden, How could I solve it?
struct ContentView: View {
let arrayOfHours: [Int] = Array(0...23)
var body: some View {
Rectangle()
.fill(Color.black.opacity(0.25))
.frame(height: 200, alignment: .center)
.padding()
.background(
VStack(alignment: .center, spacing: 4) {
ForEach (arrayOfHours.indices, id: \.self) { index in
Text(arrayOfHours[index].description)
.font(Font.body.bold())
}
}
.background(Color.yellow)
)
}
}
If I understood your description correctly you need clipped, like
struct ContentView: View {
let arrayOfHours: [Int] = Array(0...23)
var body: some View {
Rectangle()
.fill(Color.black.opacity(0.25))
.frame(height: 200, alignment: .center)
.background(
VStack(alignment: .center, spacing: 4) {
ForEach (arrayOfHours.indices, id: \.self) { index in
Text(arrayOfHours[index].description)
.font(Font.body.bold())
}
}
.background(Color.yellow)
)
.clipped() // << here !!
.padding() // should be moved here !!
}
}

SwiftUI: Show specific views with an animation delay

I'm trying to achieve the following animation: When I tap on a rectangle the rectangle should be expanded to the full width with an close button in the corner and below this rectangle a ScrollView should appear. This works so far without any problems. Now I would like to display the ScrollView a little bit later then the expanded rectangle. So when I tap on the rectangle: First the expanded rectangle with the close button should appear and 3 seconds later the ScrollView.
struct Playground: View {
#Namespace var namespace
#State var show = false
private let gridItems = [GridItem(.flexible())]
var body: some View {
if show {
VStack{
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: show)
.frame(height: 300)
.frame(maxWidth: .infinity)
Image(systemName: "xmark")
.font(.system(size: 25))
.foregroundColor(.white)
.background(Color.red)
.padding(20)
.onTapGesture {
withAnimation(.spring()){
self.show = false
}
}
}
// SHOW THIS SCROLLVIEW 3 SECONDS LATER
ScrollView{
LazyVGrid(columns: gridItems){
ForEach(0..<10){ cell in
Text("\(cell)")
}
}
}
.animation(Animation.spring().delay(3)) // doesn't work!
}
} else {
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: !show)
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation(.spring()){
self.show = true
}
}
}
}
}
We need to make separated animation (and related state) for ScrollView in this scenario.
Here is possible approach. Tested with Xcode 12.1 / iOS 14.1
struct Playground: View {
#Namespace var namespace
#State var show = false
private let gridItems = [GridItem(.flexible())]
#State private var showItems = false
var body: some View {
if show {
VStack{
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: show)
.frame(height: 300)
.frame(maxWidth: .infinity)
Image(systemName: "xmark")
.font(.system(size: 25))
.foregroundColor(.white)
.background(Color.red)
.padding(20)
.onTapGesture {
withAnimation(.spring()){
self.show = false
}
}
}
VStack {
if showItems {
ScrollView{
LazyVGrid(columns: gridItems){
ForEach(0..<10){ cell in
Text("\(cell)")
}
}
}
} else {
Spacer()
}
}
.onAppear { showItems = true }
.onDisappear { showItems = false }
.animation(Animation.spring().delay(3), value: showItems)
}
} else {
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: !show)
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation(.spring()){
self.show = true
}
}
}
}
}

How I can have transform animation in SwiftUI?

Here I got 2 shapes, one Rectangle and one Circle, which with action of a Button only one of them became visible to user, I tried to use #Namespace for this transform, but did not panned out!
MY Goal: Having a nice and smooth transform animation from one Shape to other.
struct ContentView: View {
#State var action: Bool = false
#Namespace var sameShape
var body: some View {
ZStack
{
Group
{
if action
{
Circle()
.fill(Color.blue).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "Dakota148Zero", in: sameShape)
}
else
{
Rectangle()
.fill(Color.red).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "Dakota148Zero", in: sameShape)
}
}
.animation(.easeInOut)
VStack
{
Spacer()
Button("transform") { action.toggle() }.font(Font.largeTitle).padding()
}
}
}
}
Here is way to do it by representing the circle and square as a RoundedRectangle with different cornerRadius values:
struct ContentView: View {
#State var action = false
var body: some View {
ZStack
{
RoundedRectangle(cornerRadius: action ? 0 : 75)
.fill(action ? Color.red : .blue)
.frame(width: 150, height: 150, alignment: .center)
.animation(.easeInOut)
VStack
{
Spacer()
Button("transform") {
action.toggle()
}
.font(Font.largeTitle)
.padding()
}
}
}
}
Group is not real container, so don't store animation. Replace Group with some stack, like
VStack
{
if action
// ... other code no change
With iOS14 out, you can use matchedGeometryEffect(). If you are using iOS14, I would recommend this approach.
https://www.hackingwithswift.com/quick-start/swiftui/how-to-synchronize-animations-from-one-view-to-another-with-matchedgeometryeffect
https://developer.apple.com/documentation/swiftui/view/matchedgeometryeffect(id:in:properties:anchor:issource:)
So in your solution, if you replace action.toggle() with withAnimation{self.action.toggle()} in your button code, it will animate.
Button("transform") {
withAnimation{self.action.toggle()}
}
.font(Font.largeTitle).padding()
This solution works on the simulator for me (Xcode 12.1, iPhone 11 iOS 14.1):
import SwiftUI
struct ContentView: View {
#State var action: Bool = false
#Namespace var transition
var body: some View {
ZStack {
Group {
if action {
Circle()
.fill(Color.blue).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "shape", in: transition)
} else {
Rectangle()
.fill(Color.red).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "shape", in: transition)
}
}
.animation(.easeInOut)
VStack {
Spacer()
Button("transform") { withAnimation{self.action.toggle()} }.font(Font.largeTitle).padding()
}
}
}
}
The matchedGeometryEffect() doesn't want to animate different shapes (including cornerRadius) or colors that nicely, not sure if this is a bug that will get fixed in future patches or just a feature that needs to be worked around by regular animations. With me playing around with matchedGeometryEffect(), it seems to do great with sizing things up and down, like shown with this code:
import SwiftUI
struct ContentView: View {
#State private var animate: Bool = false
#Namespace private var transition
var body: some View {
VStack {
if animate {
RoundedRectangle(cornerRadius: 75.0)
.matchedGeometryEffect(id: "shape", in: transition)
.frame(width: 250, height: 250, alignment: .center)
.foregroundColor(Color.blue)
.animation(.easeInOut)
.onTapGesture {
animate.toggle()
}
} else {
// Circle
RoundedRectangle(cornerRadius: 75.0)
.matchedGeometryEffect(id: "shape", in: transition)
.frame(width: 150, height: 150, alignment: .center)
.foregroundColor(Color.red)
.animation(.easeInOut)
.onTapGesture {
animate.toggle()
}
}
}
}
}