Creating a 3 pane Interface in swiftUI using 2 HSplitViews - macos

This is for MacOS. I am trying to make a fairly standard 3 pane interface much like you see in Xcode and other apps where the centre view has priority when resizing the window. The two side views are resizable but should stay the same size when the window is resized. I have the following example which does what I want but there is a weird resizing artefact when I resize the window, but ONLY when I make the window smaller ( actually only when it is made narrower ):
struct ContentView: View {
var body: some View {
GeometryReader{geometry in
HSplitView(){
Rectangle().foregroundColor(.red).frame(minWidth:200, idealWidth: 200, maxWidth: .infinity)//.layoutPriority(0)
HSplitView(){
Rectangle().foregroundColor(.black).frame(minWidth:200, idealWidth: geometry.size.width, maxWidth: .infinity).layoutPriority(1)
Rectangle().foregroundColor(.green).frame(minWidth:200, idealWidth: 200, maxWidth: .infinity)
}
}.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
When making the window narrower, the left side red rectangle seems to take priority over the centre rectangle causing a flicker as the red rectangle flips between two widths. I have tried various things with layoutPriority and a few other things but the problem persists. Any help with this would be much appreciated.

Well, I'll be. After struggling with this on and off for weeks, an hour after I asked the question I have appeared to solve it! Simply set the layoutPriority of the second HSplitView to 1 and the centre view to 1 as well. Makes sense when you think about it:
struct ContentView: View {
var body: some View {
GeometryReader{geometry in
HSplitView(){
Rectangle().foregroundColor(.red).frame(minWidth:200, idealWidth: 200, maxWidth: .infinity)
HSplitView(){
Rectangle().foregroundColor(.black).layoutPriority(1)
Rectangle().foregroundColor(.green).frame(minWidth:200, idealWidth: 200, maxWidth: .infinity)
}.layoutPriority(1)
}.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
So simple. Loving SwiftUI!

Related

How to scale an image to fill the parent view without affecting the layout in SwiftUI?

Goal
I often have the case where I want to scale an image so that it fills its container proportionally without modifying its frame. An example is seen in the first screenshot below. The container view has a fixed frame as indicated by the red border. The image itself has a different aspect ratio. It's scaled to fill the entire container, but it still has the same frame as the container and thus does not affect the container's layout (size).
My Solution
I used the following code to accomplish this:
struct ContentView: View {
var body: some View {
ImageContainerView()
.frame(width: 300, height: 140)
.border(.red, width: 2)
}
}
struct ImageContainerView: View {
var body: some View {
GeometryReader { geometry in
Image("image")
.resizable()
.aspectRatio(contentMode: .fill)
.border(.blue, width: 2)
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
}
}
Problem
As you can see, I used a geometry reader to manually set the image's frame to match the parent view's frame. This feels a bit superfluous as the image already receives this information implicitly as the proposed size from its parent view. However, if I don't use the geometry reader this way, the image is not clipped and its frame matches the full scaled image, not the parent view's frame. This is shown in the second screenshot below.
Question
Is there a (more "native") way in SwiftUI to achieve the desired behavior without using a geometry reader?
Here is a way to do it without using GeometryReader. By making the image an .overlay() of another view, that view can handle the clipping with the .clipped() modifier:
struct ContentView: View {
var body: some View {
ImageContainerView()
.frame(width: 300, height: 140)
.border(.red, width: 2)
}
}
struct ImageContainerView: View {
var body: some View {
Color.clear
.overlay (
Image("image")
.resizable()
.aspectRatio(contentMode: .fill)
.border(.blue, width: 2)
)
.clipped()
}
}
Also, pass in the name of the image so that ImageContainerView and be reused with other images.
Just move .clipped() from the child view to the parent. That is where you want the image clipped:
struct ContentView: View {
var body: some View {
ImageContainerView()
.frame(width: 300, height: 140)
.border(.red, width: 2)
.clipped() // Here
}
}
struct ImageContainerView: View {
var body: some View {
Image("island")
.resizable()
.aspectRatio(contentMode: .fill)
.border(.blue, width: 2)
}
}

How to set a max width on a subview of HSplitView

I'm having some trouble with HSplitView on macOS, and I'm not sure if it's a bug or something I'm doing wrong.
I'm trying to create an app with a [Sidebar | Main Content | Inspector] layout. This is similar to the SF Symbols app, where you can view more information about a selected icon.
The approach I've taken is to have the Main Content and Inspector views be in an HSplitView. Having three views in a NavigationView only seems to use the Mail pattern of [List | List | Detail], which is not the layout I'm looking for.
The problem I'm seeing is that when I set a maxWidth on the Inspector view and drag the divider to the left, the Main Content view resizes to be smaller than the available space. The parent HSplitView doesn't resize, so I'm not sure if I'm using the wrong modifiers or using them in the wrong places.
I've setup a basic test case on github. If y'all could offer and help or guidance, that would be amazing.
Observed behaviour
Expected behaviour
import SwiftUI
struct A: View {
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Text("Pane A")
Spacer()
}
Spacer()
}
.frame(
minWidth: 200,
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .leading
)
.background(Color.red)
.foregroundColor(.black)
.layoutPriority(1)
}
}
struct B: View {
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Text("Pane B")
Spacer()
}
Spacer()
}
.frame(
minWidth: 200,
idealWidth: 250,
maxWidth: 300,
maxHeight: .infinity
)
.background(Color.purple)
.foregroundColor(.black)
}
}
struct ContentView: View {
var body: some View {
HSplitView {
A()
B()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
maxHeight: .infinity
)
.background(Color.blue)
}
}e
Thanks!
I've had the same problem for several days, and I solved it in a flash.
Need to drag View
HSplitView {
View1()
View2()
}
Intuitively, we will set the maximum width of View1, but when SwiftUI drags, View1 exceeds the maximum width, it will display an inexplicable error
HSplitView {
View1()
.frame(maxWidth: 300)
View2()
}
Solutions
Set the minimum width of View2, and let View automatically fill all spaces. There will be no drag problem
HSplitView {
View1()
View2()
.frame(minWidth: 500)
}
Embedding one split view inside other may work.
Some problems arise if using GeometryReader.
HSplitView
{
View1()
HSplitView
{
View2()
View3()
}
}

SwiftUI RectangularFullView - Aligning nested Stacks

I'm building a personal apple watch app, and I've just wrapped my head around complications.
I've managed to get mine to display with the proper data, but the View is not aligning on the screen as I would like it.
The stacks are aligning in the center with dynamic widths. The text is also centre aligned.
I tried editing the text with .multilineTextAlignment but that didn't change anything.
I have mocked up the expected alignment below:
The red box is a HStack with an image that is always 60x56 pixels.
I want this to be left aligned, which seems to be working so far.
I also want it to be centered vertically in the view, but I'm not sure if it is even possible.
The pink box is a VStack of text.
This text should be left aligned, but I can't get that to work.
The blue box is the encompassing HStack
Here is my code for this complication (I've just realized I've given the template code with images of a real example, but it should be understandable):
struct ComplicationViewTemplate: View {
var body: some View {
HStack {
HStack {
Image("0i")
.aspectRatio(contentMode: .fit)
}.frame(width: 60, height: 56, alignment: .leading)
VStack {
Text("Egg")
.font(.body)
.bold()
Text("0 steps")
.font(.caption)
Text("Level 01")
.font(.caption)
}
}
}
}
If it helps, since this app is only for me, it's only ever going to be on a Series 5 44mm watch, so items wouldn't have to be dynamic for other watch sizes.
Summary of questions:
Is it possible to vertically centre a HStack inside another HStack
Is it possible to left-align text inside of a VStack
Thanks for any help!
I've managed to get this to work:
struct ComplicationViewTemplate: View {
var body: some View {
HStack {
HStack {
Image("14gi")
.interpolation(.none)
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 56, alignment: .leading)
}.frame(width: 60, height: 56, alignment: .leading)
Spacer(minLength: 10)
VStack(alignment: .leading) {
Text("Pikachu").bold().font(.body).alignmentGuide(.leading, computeValue: { d in d[.leading]
})
Text("000000 steps").font(.caption).alignmentGuide(.leading, computeValue: { d in d[.leading]
})
Text("Level 00").font(.caption).alignmentGuide(.leading, computeValue: { d in d[.leading]
})
}.frame(width: 110, alignment: .leading)
}
}
}
Here's what the result looks like (I've also updated the template slightly):

SwiftUI: resize View to the content size

I have some large view full of some sort of subViews
I don't know which size of content of the view
How to:
Automatically resize view to it's content
Set maxSize of window? (If content is larger than this size, window must will have this size, but not content size.)
At the moment I think about following direction:
Wrap it with frame
.frame(minWidth: 450, idealWidth: 1000, maxWidth: .infinity, minHeight: 250, idealHeight: 1000, maxHeight: .infinity)
to use GeometryReader go get parent size
but I don't know how to do this correctly

SwiftUI pin Image to top right of screen and resize based on device size

I just learning Apple's SwiftUI and it is a bit frustrating even doing the basics. I am trying to get my image to the top right of my screen but the default has it showing up in the center of my screen. Anyone know how I can accomplish this. In addition, I tried to get the image to resize based on the screen size but the old self.frame.width that I used to use in regular Swift doesn't work in Swift UI. Sorry, but this new way of writing code in SwiftUI is very odd to me.
var body: some View {
ZStack {
//Define a screen color
LinearGradient (gradient: Gradient(colors:[Color(ColorsSaved.gitLabDark),Color(ColorsSaved.gitLabLight)]),startPoint: .leading,endPoint: .trailing)
//Extend the screen to all edges
.edgesIgnoringSafeArea(.all)
VStack(alignment: .trailing) {
Image("blobVectorDark")
.resizable()
.edgesIgnoringSafeArea(.top)
.frame(width: 60, height: 60)
// .offset(x: , y: 20)
}
}
}
}
You need to add a frame for the outer container, here is the VStack.
Then assign the alignment for this container.
The width and height in the frame of VStack needs to use geometryProxy.
GeometryReader{ (proxy : GeometryProxy) in // New Code
VStack(alignment: .trailing) {
Image("blobVectorDark")
.resizable()
.edgesIgnoringSafeArea(.top)
.frame(width: 60, height: 60)
// .offset(x: , y: 20)
}
.frame(width: proxy.size.width, height:proxy.size.height , alignment: .topLeading) // New Code
}
I'm learning too so this might not be the ideal way to do it, but I did manage to get the image to pin to the top right of the screen.
You can get the screen width from GeometryReader. The alignment was driving me nuts for a while, until I realized I could give a frame to a Spacer to take up the other side of an HStack, and then it worked.
var body: some View {
GeometryReader { geometry in
VStack {
HStack {
Spacer()
.frame(width: geometry.size.width / 2)
Image("blobVectorDark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geometry.size.width / 2)
}
Spacer()
}
}
.edgesIgnoringSafeArea(.all)
}

Resources