In a MacOS app, I have a LazyVGrid. As we resize the window width, it adapts the layout, i.e. rearranges its items.
I would like to animate these layout changes. Afaik the recommended ways are withAnimation and .animation(Animation?, value: Equatable), but grid layout is automatic based on its container, it does not depend on a variable that I manage. For my use, .animation(Animation?) would be suitable, but it is now deprecated - it still works, though.
Shall I manage some state variable, e.g. a windowWidth (which I update on window-resize events), just for this animation? Or do you know a better way?
import SwiftUI
#main
struct MacosPlaygroundApp: App {
let colors: [Color] = [.red, .pink, .blue, .brown, .cyan, .gray, .green, .indigo, .orange, .purple]
let gridItem = GridItem(
.adaptive(minimum: 100, maximum: 100),
spacing: 10,
alignment: .center
)
var body: some Scene {
WindowGroup {
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: [gridItem], alignment: .center, spacing: 10) {
ForEach(colors, id: \.self) { color in
color
.contentShape(Rectangle())
.cornerRadius(4)
.frame(width: 100, height: 100)
}
}.animation(.easeIn)
}
.frame(minWidth: 120, maxWidth: .infinity, minHeight: 120, maxHeight: .infinity)
}
}
}
You can make the columns as state in this view and calculate the values within the withAnimation block.
Like this:
#State var gridItems: [GridItem] = []
var body: some View {
GeometryReader { proxy in
ScrollView(.vertical) {
LazyVGrid(columns: gridItems) {
itemGrid()
}
}.onChange(of: proxy.size) { newValue in
if gridItems.isEmpty {
updateGridItems(newValue.width)
} else {
withAnimation(.easeOut(duration: 0.2)) {
updateGridItems(newValue.width)
}
}
}.onAppear {
if gridItems.isEmpty {
updateGridItems(proxy.size.width)
}
}
}
}
You can access the full example in my app in GitHub: https://github.com/JuniperPhoton/MyerTidy.Apple/blob/main/Shared/View/BatchOperationSelectView.swift#L47
Related
I'm trying to use my custom card view as label to navigation link. this navigation link is also a Grid item.
The result is a
My Card Code:
struct CityCard: View {
var cityData: CityData
var body: some View {
VStack {
Text("\(cityData.name)")
.padding([.top], 10)
Spacer()
Image(systemName: cityData.forecastIcon)
.resizable()
.frame(width: 45, height: 45)
Spacer()
HStack(alignment: .bottom) {
Text(String(format: "%.2f $", cityData.rate))
.padding([.leading, .bottom], 10)
Spacer()
Text(String(format: "%.2f °", cityData.degrees))
.padding([.trailing, .bottom], 10)
}
}.frame(width: 150, height: 150)
.background(Color.blue)
.cornerRadius(10)
.navigationTitle(cityData.name)
}
}
My List View:
struct CityList: View {
var cities: [CityData]
let columns = [
GridItem(.flexible(minimum: 150, maximum: 150)),
GridItem(.flexible(minimum: 150, maximum: 150))
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(self.cities) { item in
NavigationLink(destination: Text(item.name), label: {
CityCard(cityData: item)
})
}
}
}
}
}
someone has a solution why it gives me this opacity?
Update:
The main contact view is:
It's greyed out because you don't yet have a NavigationView in your view hierarchy, which makes it possible to actually navigate to a new view.
You can, for example, wrap your ScrollView in a NavigationView:
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(self.cities) { item in
NavigationLink(destination: Text(item.name), label: {
CityCard(cityData: item)
})
}
}
}
}
}
Or, if it's only happening in your preview, add a NavigationView to your preview code:
NavigationView {
CityList(...)
}
Keep in mind that if you have the default accent color (blue, in light mode) that your links will become invisible (since they will be blue) on top of the blue background. You can set a custom accent color in your project or via the accentColor modifier:
NavigationView {
//content
}.accentColor(.black)
I am trying to get familiar with LazyVGrid, layout and frames/coordinate spaces in SwiftUI and draw a grid that has 4 columns, each being 1/4 the width of the screen. On top of that, on cell tap, I want to place a view exactly over the tapped cell and animate it to full screen (or a custom frame of my choice).
I have the following code:
struct CellInfo {
let cellId:String
let globalFrame:CGRect
}
struct TestView: View {
var columns = [
GridItem(.flexible(), spacing: 0),
GridItem(.flexible(), spacing: 0),
GridItem(.flexible(), spacing: 0),
GridItem(.flexible(), spacing: 0)
]
let items = (1...100).map { "Cell \($0)" }
#State private var cellInfo:CellInfo?
var body: some View {
GeometryReader { geoProxy in
let cellSide = CGFloat(Int(geoProxy.size.width) / columns.count)
let _ = print("cellSide: \(cellSide). cellSide * columns: \(cellSide*CGFloat(columns.count)), geoProxy.size.width: \(geoProxy.size.width)")
ZStack(alignment: .center) {
ScrollView(.vertical) {
LazyVGrid(columns: columns, alignment: .center, spacing: 0) {
ForEach(items, id: \.self) { id in
CellView(testId: id, cellInfo: $cellInfo)
.background(Color(.green))
.frame(width: cellSide, height: cellSide, alignment: .center)
}
}
}
.clipped()
.background(Color(.systemYellow))
.frame(maxWidth: geoProxy.size.width, maxHeight: geoProxy.size.height)
if cellInfo != nil {
Rectangle()
.background(Color(.white))
.frame(width:cellInfo!.globalFrame.size.width, height: cellInfo!.globalFrame.size.height)
.position(x: cellInfo!.globalFrame.origin.x, y: cellInfo!.globalFrame.origin.y)
}
}
.background(Color(.systemBlue))
.frame(maxWidth: geoProxy.size.width, maxHeight: geoProxy.size.height)
}
.background(Color(.systemRed))
.coordinateSpace(name: "testCSpace")
.statusBar(hidden: true)
}
}
struct CellView: View {
#State var testId:String
#Binding var cellInfo:CellInfo?
var body: some View {
GeometryReader { geoProxy in
ZStack {
Rectangle()
.stroke(Color(.systemRed), lineWidth: 1)
.background(Color(.systemOrange))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(
Text("cell: \(testId)")
)
.onTapGesture {
let testCSpaceFrame = geoProxy.frame(in: .named("testCSpace"))
let localFrame = geoProxy.frame(in: .local)
let globalFrame = geoProxy.frame(in: .global)
print("local frame: \(localFrame), globalFrame: \(globalFrame), testCSpace: \(testCSpaceFrame)")
let info = CellInfo(cellId: testId, globalFrame: globalFrame)
print("on tap cell info: \(info)")
cellInfo = info
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Color(.systemGray))
}
}
The outermost geometry proxy gives this size log:
cellSide: 341.5. cellSide * columns: 1366.0, geoProxy.size.width:
1366.0
This is what gets rendered:
When I tap on cell 1 for example, this is what gets logged:
local frame: (0.0, 0.0, 341.0, 341.0), globalFrame: (0.25, 0.0, 341.0,
341.0), testCSpace: (0.25, 0.0, 341.0, 341.0) on tap cell info: CellInfo(cellId: "Cell 1", globalFrame: (0.25, 0.0, 341.0, 341.0))
When tapping on "Cell 6" this is what the newly rendered screen looks like:
So, given this code:
how can I get the (white) overlaid view's frame to match the cell's view's frame I tapped on? The width and height seem ok, but the position is off. (What am I doing wrong?)
How can I get the white view to animate to full screen once placed right on top of the tapped cell?
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 !!
}
}
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()
}
}
}
}
}
I'm researching SwiftUI to see if it fits our compony's use case. However, I find it rather bare bones and wonder wether I am missing some key insights.
Currently I am researching if V/HStack's can respond dynamically to Accesability font-size changes.
The issue is that I need to give the items a frame, it does not automatically pin the outer Stacks items-view constraints to their inner content. When the user sets the font-size to full blown Bananas-mode the UI just breaks.
struct PatientHistoryView: View {
#State var model : PatientHistoryModel = historyModel;
var body : some View {
GeometryReader { g in
ScrollView(.vertical, showsIndicators: true) {
List
{
ForEach(self.model.data)
{ labResults in
LabDetailView(labeResults: labResults)
}
}.frame(width: 300, height: g.size.height).background(Color.clear)
}
}
}
}
adding these properties do not seem to affect the Font-scaling issues:
struct PatientHistoryView: View {
#State var model : PatientHistoryModel = historyModel;
var body : some View {
GeometryReader { g in
ScrollView(.vertical, showsIndicators: true) {
List
{
ForEach(self.model.data)
{ labResults in
LabDetailView(labeResults: labResults).frame(minWidth: 200, idealWidth: 400, maxWidth: 600, minHeight: 50, idealHeight: 100, maxHeight: 600)
}
}.frame(width: 300, height: g.size.height).background(Color.clear)
}
}
}
}
Here is the labDetailView
struct LabDetailView: View {
var labeResults : LabResults
var color : Color = .red
var spacer : some View {
Spacer().frame(width: 10, height: 0)
}
var body : some View {
GeometryReader { g in
HStack
{
Group
{
VerticalLineView().frame(width: 10, height: g.size.height, alignment: .leading).background(self.color)
self.spacer
self.spacer
LineStack(lineColor: .white, lineHeight: 2).frame(width: 20, height: 20, alignment: .center)
self.spacer
self.spacer
}
VStack(alignment: .leading, spacing: 8)
{
Text(self.labeResults.title).fontWeight(.bold)
Text(self.labeResults.type)
}
Spacer()
VStack(alignment: .center)
{
Text(self.labeResults.max.asString)
Spacer()
Text(self.labeResults.avg.asString).fontWeight(.bold).font(.system(size: 20))
Spacer()
Text(self.labeResults.min.asString)
}.frame(width: 50, height: g.size.height, alignment: .center)
self.spacer
self.spacer
}.frame(width: g.size.width, height: g.size.height, alignment: .leading)
}
}
}
In UIkit these issues can be resolved in the traitCollectionDidChange hook. However, in Swift UI I just cannot find an equivalent.
Doe anybody have any ideas?
Thanks!