SwiftUI window size for document based MacOs App - swiftui

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.

Related

SwiftUI: Animate LazyVGrid layout change when window is resized

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

SwiftUI shape fill body

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.

How to change the height of the object by using DragGesture in SwiftUI?

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)

SwiftUI: How to constrain image size?

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)
}
}

SwiftUI, is there a traitCollectionDidChange-like method in a View?

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!