I'm trying to construct a view in SwiftUI, where the user can keep zooming in and out, and show elements across the view. But the rectangle keeps the size of the window, and scales down when zooming out instead of filling the body. The body (black) correctly fills the window.
How do you make the white rectangle fill the body when zooming out?
(Must be run in an app instead of preview)
import SwiftUI
func rgb (_ count: Int) -> [Color]{
let colors = [Color.red, Color.green, Color.blue]
var arr: [Color] = []
for i in 0..<count {
arr.append(colors[i%3])
}
return arr
}
struct ContentView: View {
#State var scale: CGFloat = 1.0
var body: some View {
let colors = rgb(20)
ZStack {
Rectangle()
.fill(Color.white)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .center)
ForEach(colors.indices.reversed(), id: \.self) { i in
Circle()
.size(width: 100, height: 100)
.fill(colors[i])
.offset(x: 100.0*CGFloat(i), y: 100.0*CGFloat(i))
}
}
.drawingGroup()
.scaleEffect(scale)
.gesture(MagnificationGesture()
.onChanged {self.scale = $0})
.background(Color.black)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .center)
}
}
I put this an an answer to show a screenshot. The second bit of code behaves very inconsistently. I never see 20 circles. It will zoom, but seems to then be caught in some other view. It is very strange behavior and tough to explain. While the screenshot is here, I could run it 20 times and get 20 different screenshots if I zoom and/or resize the window. I am not on Apple silicon, so your first post may be a bug in implementation on Apple silicon. Wouldn't be the first.
Functioning example for this use case, with rectangle removed from ZStack:
import SwiftUI
func rgb (_ count: Int) -> [Color]{
let colors = [Color.red, Color.green, Color.blue]
var arr: [Color] = []
for i in 0..<count {
arr.append(colors[i%3])
}
return arr
}
struct ContentView: View {
#State var scale: CGFloat = 1.0
#State var colorIndex = 0
var bgColor: Color { rgb(3)[colorIndex%3] }
var body: some View {
let colors = rgb(20)
ZStack {
ForEach(colors.indices.reversed(), id: \.self) { i in
Circle()
.size(width: 100, height: 100)
.fill(colors[i])
.offset(x: 100.0*CGFloat(i), y: 100.0*CGFloat(i))
}
}
.drawingGroup()
.scaleEffect(scale)
.background(bgColor)
.gesture(MagnificationGesture()
.onChanged {scale = $0})
.gesture(TapGesture().onEnded({colorIndex+=1}))
}
}
However it does not fix the problem of the shape not scaling to the body size.
Related
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
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'm trying to increase the height of the shape by using the "dragger" (rounded grey rectangle) element. I use the DragGesture helper provided by SwiftUI to get the current position of the user finger. Unfortunately, during the drag event content is jumping for some reason.
Could you please help me to find the root cause of the problem?
This is how it looks during the drag event
If I remove Spacer() everything is okay but not what I wanted
This is my code snippets
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
CustomDraggableComponent()
Spacer() // If comment this line the result will be as on the bottom GIF example
}
}
import SwiftUI
let MIN_HEIGHT = 50
struct CustomDraggableComponent: View {
#State var height: CGFloat = MIN_HEIGHT
var body: some View {
VStack {
Rectangle()
.fill(Color.red)
.frame(width: .infinity, height: height)
HStack {
Spacer()
Rectangle()
.fill(Color.gray)
.frame(width: 100, height: 10)
.cornerRadius(10)
.gesture(
DragGesture()
.onChanged { value in
height = value.translation.height + MIN_HEIGHT
}
)
Spacer()
}
}
}
The correct calculation is
.gesture(
DragGesture()
.onChanged { value in
height = max(MIN_HEIGHT, height + value.translation.height)
}
)
Also, remove the infinite width. It's invalid.
.frame(minHeight: 0, maxHeight: height)
or
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: height)
I am trying to constrain image size between min and max. But the view expands both width and height to their max values, removing the original aspect ratio.
import SwiftUI
struct ImageConstrainSizeTest: View {
var body: some View {
Image("bike")
.resizable()
.aspectRatio(contentMode: .fit)
.border(Color.yellow, width: 5)
.frame(minWidth: 10, maxWidth: 300, minHeight: 10, maxHeight: 300, alignment: .center)
.border(Color.red, width: 5)
}
}
struct ImageConstrainSizeTest_Previews: PreviewProvider {
static var previews: some View {
ImageConstrainSizeTest()
}
}
In the screenshot below, I want the red box to shrink to yellow box.
Tried using GeometryReader, but that gives the opposite effect of expanding the yellow box to red box.
Any thoughts?
Here is a demo of possible approach - using view preferences and a bit different layout order (we give area to fit image with aspect and then by resulting image size constrain this area).
Demo prepared & tested with Xcode 11.7 / iOS 13.7
struct ViewSizeKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: CGSize = .zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
struct ImageConstrainSizeTest: View {
#State private var size = CGSize.zero
var body: some View {
Color.clear
.frame(width: 300, height: 300)
.overlay(
Image("img")
.resizable()
.aspectRatio(contentMode: .fit)
.border(Color.yellow, width: 5)
.background(GeometryReader {
Color.clear.preference(key: ViewSizeKey.self,
value: $0.size) })
)
.onPreferenceChange(ViewSizeKey.self) {
self.size = $0
}
.frame(maxWidth: size.width, maxHeight: size.height)
.border(Color.red, width: 5)
}
}
I am moving my app from SwiftUI xCode 11 to the new Document Based App lifecycle format in xCode 12. I have not been able to figure out how to set the window size. In Xcode 11 I had
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
How do I achieve the same effect with no AppDelegate?
Try the following (as window by default has .fullSizeContentView)
var body: some Scene {
WindowGroup {
ContentView()
.frame(width: 1200, height: 800) // << here !!
.frame(minWidth: 800, maxWidth: .infinity,
minHeight: 600, maxHeight: .infinity)
}
}
In macOS 13.0.1, this works:
var
body: some Scene
{
DocumentGroup(newDocument: MyDocument()) // Or WindowGroup
{ file in
ContentView(document: file.$document)
}
.defaultSize(width: 200, height: 400) // <-- Set your default size here.
}
Note that app state restoration might confuse matters, but creating a new document (or deleting app state) should create a new window with the specified default.
I used the trick of swapping out the minimum size. It's not elegant, but it solves the problem.
struct ContentView: View
{
#ObservedObject var document: MyDocument
#State var minSize:CGSize = {
// Set your preferred initial size here
guard let screen = NSScreen.main?.frame.size
else { return CGSize(width: 800, height: 600) }
return CGSize(width: screen.width * 0.75, height: screen.height * 0.75)
}()
var body: some View
{
VStack
{
Text("Content")
}
.frame(minWidth: minSize.width, minHeight: minSize.height)
.onAppear {
Task {
await Task.sleep(500_000_000)
// Set your real min size here
minSize = CGSize(width: 600, height: 400)
}
}
}
}
I hope the next SwiftUI will have a normal way.