AppKit's view bridged to swiftui causes view to stretch outside view/window frame - swift

I'm having layout issues with mixed AppKit(bridged to SwiftUI)/SwiftUI views. I have simple view setup containing 2 rows where row is defined as: text view + text field from app kit bridged to swiftui.
I need my text views left of the text field to be aligned to the right, a.k.a it looks like they have trailing alignment. But since my view hierarchy defines views as rows and not as VStacks, I need customized alignment guide.
It all works fine when I replace bridged view with some SwiftUI's native view i.e Text().
Any ideas what may be causing this and how to fix it? I've tried to play around with setting content compression & hugging priorities on AppKit view or layout priorities for both SwiftUI + AppKit to SwiftUI views, without success.
Setting some specific frame size may have achieve somehow what I want, but since my texts on the left side are localized and may be arbitrarily long (and I basically want them to show them whole without truncating) and whole view may be resized horizontally too, it's not a good solution.
Here's very simplified code, that does illustrate the issue:
import AppKit
import Combine
import SwiftUI
extension HorizontalAlignment {
struct TrailingTextContent: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let trailingTextContent = HorizontalAlignment(TrailingTextContent.self)
}
struct TextField: NSViewRepresentable {
#Binding private var value: String
init(value: Binding<String>) {
self._value = value
}
func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField(frame: .zero)
return textField
}
func updateNSView(_ nsView: NSTextField, context: Context) {}
}
struct DetailsView: View {
var body: some View {
VStack(alignment: .trailingTextContent, spacing: 8) {
HStack(alignment: .center, spacing: 0) {
Text("Long")
.padding(.trailing, 8)
.alignmentGuide(.trailingTextContent) { d in d[HorizontalAlignment.trailing] }
TextField(value: .constant("asdasdasdasdasdasdada"))
}
HStack(alignment: .center, spacing: 0) {
Text("A bit longer text")
.padding(.trailing, 8)
.alignmentGuide(.trailingTextContent) { d in d[HorizontalAlignment.trailing] }
TextField(value: .constant("asdasdasdasdasdasdada"))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
DetailsView()
}
}
This is what it looks like:
and this is sort of how I want it to look like (+ text fields should be both same length) - I've applied some padding to whole view

Related

Extend SwiftUI's TextEditor within Form to use remaining space

I'm using a TextEditor inside a Form. A minimal playground example would be
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var text = String()
var body: some View {
Form {
Section("Section") {
Toggle("Toggle", isOn: .constant(true))
LabeledContent("TextEditor") {
TextEditor(text: $text)
}
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
This renders into something like:
As TextEditor is a multiline input field, I'd like it to extend to the remaining available space on the screen, so something like
I can achieve this by adding a .frame(height:540) modifier to the TextEditor, however this hardcodes the extend is not very dynamic and thus only works on a specific device.
So the question is, how to to this in a dynamic way which works on all potential devices (different iPhones, iPad, ...).
Note: This question is similar to SwiftUI Texteditor in a form. However this only addresses the issue how to get it to show multiple lines, which can be easily achieved using the above mentioned .frame(height:X) modifier.
I don't think this is the best solution, although it works. If you want something else, try using GeometryReader with a dynamic height value stored in a CGFloat variable. (Tested on iPhone 14 Pro, 14 Pro Max and iPad 12.9 inch)
UISCREEN SOLUTION
import SwiftUI
struct ContentView: View {
#State var text = String()
var body: some View {
if #available(iOS 16, *){
Form {
Section("Section") {
Toggle("Toggle", isOn: .constant(true))
LabeledContent("TextEditor") {
TextEditor(text: $text)
}.frame(minHeight: UIScreen.main.bounds.maxY-190)
}
}
}
}
}
You can also use constant points, as the points reflect different screen sizes and resolutions: https://stackoverflow.com/a/73653966/14856451
GEOMETRY READER SOLUTION
#State var text = String()
#Environment(\.defaultMinListRowHeight) var minH
var body: some View {
if #available(iOS 16, *){
GeometryReader {geometry in
Form {
Section("Section") {
Toggle("Toggle", isOn: .constant(true))
LabeledContent("TextEditor") {
TextEditor(text: $text)
}.frame(minHeight: geometry.size.height-minH-70)
}
}
}
}
}
Notice the blue outline - that's the size of your form. As you can see in all three cases, the TextField reaches the bottom of the form without going into the safe zone, which is used to control gestures on the device (there are no gestures on the iPhone SE, so there is no safe zone either).

Inserting non-list elements in a SwiftUI view

I am working on a SwiftUI page that consists of a table view with some rows but I would also like to have some non-cell elements in there. I am currently having some issues with this and I have tried various different avenues. I basically just need some elements in there that aren't wrapped inside a cell while still maintaining the tableview's grayish background. In my example below, I am trying to get an image right under the table rows.
Below is my code:
import SwiftUI
struct ContentView: View {
private var color = Color(red: 32/255, green: 35/255, blue: 0/255)
var body: some View {
NavigationView {
Form {
Section() {
HStack {
Text("AA")
.foregroundColor(color)
}
HStack {
Text("B")
.foregroundColor(color)
}
HStack {
Text("C")
.foregroundColor(color)
}
HStack {
Text("D")
.foregroundColor(color)
}
HStack {
Text("E")
.foregroundColor(color)
}
HStack {
Text("F")
.foregroundColor(color)
}
HStack {
Text("G")
.foregroundColor(color)
}
}
}.navigationBarTitle(Text("Page"))
HStack {
Image(systemName: "fallLeaves")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here are all of the scenarios I have tried that have been unsuccessful:
Scenario 1: Adding a HStack with the image outside of the List. (What the current code shows) This does not get the image to show as the table view takes the whole view.
Scenario 2: Adding the HStack with the image within the list block. This wraps the image around the cell like so
Scenario 3: Wrapping the list and the HStack contains the image inside a VStack. This is no good as it's basically like a split-screen with two different views. The table gets shrunk and has it's own scroll bar. Notice the scroll bar in the image below
The ideal solution would look like this but I'm not sure what to try as I can't get it to look like this where it's all one continuous view and the image isn't in a cell
i'll let you work out the padding and the rounding.
struct ContentView: View {
private var color = Color(red: 32/255, green: 35/255, blue: 0/255)
var body: some View {
NavigationView {
Form {
Section(content: {
HStack {
Text("AA")
.foregroundColor(color)
}
HStack {
Text("B")
.foregroundColor(color)
}
}, footer: {
Image("fallLeaves")
})
}.navigationBarTitle(Text("Page"))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can also use place your component (event a page view) inside the cell.
What can be done;
Use a Section to have a good style when you initiate another cell (because we'll make the following one transparent)
Create a new Section
Place a / any view
Add .listRowBackground(Color.clear) to the new view; to see the background transparently.
Add .listRowSeparator(.hidden)to the new view; to remove the cell line below.
After these, you'll have a placed view with additional padding. If this is important, you can play with padding to catch 0 padding distance. Also, by changing the list style (for this second group) with these kinds of styles, .listStyle(.grouped), you can get catch zero padding/spacing.
The footer solution can work for images, but when you need some texts, you'll see that it'll scale down the sizes. So instead of footer, my solution is to use a cell view and show it as a non-cell item;
Here is my change, which works (at least I think)
import SwiftUI
struct ContentView: View {
private var color = Color(red: 32/255, green: 35/255, blue: 255/255)
// Just changed color to test my changes better.
// private var color = Color(red: 32/255, green: 35/255, blue: 0/255)
var body: some View {
NavigationView {
Form {
Section() {
Text("AA")
Text("B")
Text("C")
Text("D")
Text("E")
Text("F")
Text("G")
}
//foreground color won't colorize the cell's bottom lines
.foregroundColor(color)
Section{
HStack{
// Use Spacer or another horizontal alignment
// I just tried with another image, you can place yours with full width and remove the Spacer() 's
Spacer()
Image(systemName: "checkmark.seal.fill")
.resizable()
.scaledToFit()
.foregroundColor(color)
.frame(width: 100)
Spacer()
}
.listRowBackground(Color.clear)
// Not required for single items; but would be usefull, if you'll add more views or sections
.listRowSeparator(.hidden)
}
}.navigationBarTitle(Text("Page"))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is why your methods won't work;
Scenario 1: Using VStack and HStack will split the screen into different scrollable. This is by design, In SwiftUI List element overrides Stacks and ScrollableView
(Reference)
Scenario 2: Same as Scenario 1
Scenario 3: Actually, this is the same as Scenario 1
You might try 3 things;
My solution, I don't see a major problem
Footer solution, which can work, but for some views, you will have problems
Determine a background color in ZStack and develop a custom button object which looks like a cell view (cons: non-native way and long time consuming work around)

SwiftUI's anchorPreference collapses height of its view

I am using SwiftUI and I am trying to pass up the height from a subview up to its parent view. It’s my understanding to use something like PreferenceKey along with .anchorPreference and then act on the change using .onPreferenceChange.
However, due to the lack of documentation on Apple’s end, I am not sure if I am using this correctly or if this is a bug with the framework perhaps.
Essentially, I want a view that can grow or shrink based on its content, however, I want to cap its size, so it doesn’t grow past, say 300 pts vertically. After that, any clipped content will be accessible via its ScrollView.
The issue is that the value is always zero for height, but I get correct values for the width.
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
VStack {
GeometryReader { geometry in
VStack(alignment: .leading) {
content()
}
.padding(.top, 10)
.padding([.leading, .bottom, .trailing], 20)
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { geometry[$0].size })
}
}
.onPreferenceChange(SizePreferenceKey.self) { self.contentHeight = $0.height }
When you want to get size of content then you need to read it from inside content instead of outside parent available space... in your case you could do this (as content itself is unknown) from content's background, like
VStack(alignment: .leading) {
content()
}
.padding(.top, 10)
.padding([.leading, .bottom, .trailing], 20)
.background(GeometryReader { geometry in
Color.clear
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { geometry[$0].size })
})
.onPreferenceChange(SizePreferenceKey.self) { self.contentHeight = $0.height }
Note: content() should have determined size from itself, otherwise you'll get chicken-egg problem in ScrollView
Unfortunately, there seems to be no easy solution for this. I came up with this:
Anchors are partial complete values and require a GeometryProxy to return a value. That is, you create an anchor value - say a bounds property - for any child view (whose value is incomplete at this time). Then you can get the actual bounds value relative to a given geometry proxy only when you have that proxy.
With onPreferenceChange you don't get a geometry proxy, though. You need to use backgroundPreferenceValue or overlayPreferenceValue.
The idea would be now, to use backgroundPreferenceValue, create a geometry proxy and use this proxy to relate your "bounds" anchors that have been created for each view in your scroll view content and which have been collected with an appropriate preference key, storing anchor bounds values in an array. When you have your proxy and the anchors (view bounds) you can calculate the actual bounds for each view relative to your geometry proxy - and this proxy relates to your ScrollView.
Then with backgroundPreferenceValue we could set the frame of the background view of the ScrollView. However, there's a catch:
The problem with a ScrollView is, that you cannot set the background and expect the scroll view sets its frame accordingly. That won't work.
The solution to this is using a #State variable containing the height of the content, respectively the max height. It must be set somehow when the bounds are available. This is in backgroundPreferenceValue, however, we cannot set this state property directly, since we are in the view "update phase". We can workaround this problem by just using onAppear where we can set a state property.
The state property "height" can then be used to set the frame of the ScrollView directly using the frame modifier.
See code below:
Xcode Version 13.0 beta 4:
import SwiftUI
struct ContentView: View {
let labels = (0...1).map { "- \($0) -" }
//let labels = (0...9).map { "- \($0) -" }
#State var height: CGFloat = 0
var body: some View {
HStack {
ScrollView {
ForEach(labels, id: \.self) {
Text($0)
.anchorPreference(
key: ContentFramesStorePreferenceKey.self,
value: .bounds,
transform: { [$0] })
}
}
}
.frame(height: height)
.backgroundPreferenceValue(ContentFramesStorePreferenceKey.self) { anchors in
GeometryReader { proxy in
let boundss: [CGRect] = anchors.map { proxy[$0] }
let bounds = boundss.reduce(CGRect.zero) { partialResult, rect in
partialResult.union(rect)
}
let maxHeight = min(bounds.height, 100)
Color.red.frame(width: proxy.size.width, height: maxHeight)
.onAppear {
self.height = maxHeight
}
}
}
}
}
fileprivate struct ContentFramesStorePreferenceKey: PreferenceKey {
typealias Value = [Anchor<CGRect>]
static var defaultValue: Value = []
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(
NavigationView {
ContentView()
}
.navigationViewStyle(.stack)
)

Dynamic row hight containing TextEditor inside a List in SwiftUI

I have a List containing a TextEditor
struct ContentView: View {
#State var text: String = "test"
var body: some View {
List((1...10), id: \.self) { _ in
TextEditor(text: $text)
}
}
}
But it's items are not growing on height change of the TextEditor. I have tried .fixedSize() modifier with no luck. What am I missing here?
You can use an invisible Text in a ZStack to make it dynamic.
struct ContentView: View {
#State var text: String = "test"
var body: some View {
List((1...10), id: \.self) { _ in
ZStack {
TextEditor(text: $text)
Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}
}
}
}
Note that you should consider changing font size and other properties to match the TextEditor
As far as I can see from view hierarchy TextEditor is just simple wrapper around UITextView and does not have more to add, so you can huck into that layer and find UIKit solution for what you need, or ...
here is a demo of possible approach to handle it at SwiftUI level (the idea is to use Text view as a reference for wrapping behaviour and adjust TextEditor exactly to it)
Tested with Xcode 12b / iOS 14 (red border is added for better visibility)
Modified your view:
struct ContentView: View {
#State var text: String = "test"
#State private var height: CGFloat = .zero
var body: some View {
List {
ForEach((1...10), id: \.self) { _ in
ZStack(alignment: .leading) {
Text(text).foregroundColor(.clear).padding(6)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
})
TextEditor(text: $text)
.frame(minHeight: height)
//.border(Color.red) // << for testing
}
.onPreferenceChange(ViewHeightKey.self) { height = $0 }
}
}
}
}
Note: ViewHeightKey is a preference key, used in my other solutions, so can be get from there
ForEach and GeometryReader: variable height for children?
How to make a SwiftUI List scroll automatically?
Automatically adjustable view height based on text height in SwiftUI

With SwiftUI, is there a way to constrain a view's size to another non-sibling view?

I'm fiddling with a view layout - a graph - where I'm seeing how far I can get within an all-SwiftUI layout. Each of the component pieces are fine, but assembling the whole isn't working quite as I'd like. What I've found is that I can easily constrain sizing along a single stack axis - but not two axis at once (both vertical and horizontal).
I started to reach for AlignmentGuides, as I found you can align non-siblings with a custom guide. That will help my goal, but it doesn't solve the sizing part, which is the heart of this question:
Is there a way to constrain a view's size based on another, non-sibling, view?
A simplification of the structure is:
HStack {
CellOneView {
}
CellTwoView {
}
}
HStack {
CellThreeView {
}
CellFourView {
}
}
Which maps out to:
+-----+-----+
| 1 | 2 |
+-----+-----+
| 3 | 4 |
+-----+-----+
Is there a way to tell CellFour (which isn't in the same HStack as cell's 1 and 2) that I want it to constrain itself (and align) to the width of cell CellTwo?
This does not need to strictly be a grid view (example of grid view). There are really only three views that I care about in this case - the areas that roughly map to cell 1, cell 2, and cell 4. I want the heights of Cell 1 and Cell 2 to be the same (accomplished easily with the current HStack), and the widths of Cell 2 and Cell 4 to be the same - that's where I'm struggling.
1.Getting the size
struct CellTwoView: View {
var body: some View {
Rectangle()
.background(
GeometryReader(content: { (proxy: GeometryProxy) in
Color.clear
.preference(key: MyPreferenceKey.self, value: MyPreferenceData(rect: proxy.size))
})
)
}
}
Explanation - Here I have get the size of the view from using background View ( Color.clear ) , I used this trick unless getting the size from CellTwoView itself ; 'cause of SwiftUI-View size is determined by the view itself If they have size ( parent cannot change the size like in UIKit ). so if I use GeometryReader with CellTwoView itself , then the GeometryReader takes as much as size available in the parent of CellTwoView. - > reason -> GeometryReader depends on their parent size. (actually this is another topic and the main thing in SwiftUI)
Key ->
struct MyPreferenceKey: PreferenceKey {
static var defaultValue: MyPreferenceData = MyPreferenceData(size: CGSize.zero)
static func reduce(value: inout MyPreferenceData, nextValue: () -> MyPreferenceData) {
value = nextValue()
}
typealias Value = MyPreferenceData
}
Value (and how it is handle when preference change) ->
struct MyPreferenceData: Equatable {
let size: CGSize
//you can give any name to this variable as usual.
}
2. Applying the size to another view
struct ContentView: View {
#State var widtheOfCellTwoView: CGFloat = .zero
var body: some View {
VStack {
HStack {
CellOneView()
CellTwoView ()
.onPreferenceChange(MyPreferenceKey.self) { (prefereneValue) in
self.widtheOfCellTwoView = prefereneValue.size.width
}
}
HStack {
CellThreeView ()
CellFourView ()
.frame(width: widtheOfCellTwoView)
}
}
}
}
As you already started using alignment guides it is possible to with this instrument. Here is possible approach (for your scratchy example):
#State private var width: CGFloat = 10 // < initial value does not much matter
...
HStack {
CellOneView {
}
CellTwoView {
}
.alignmentGuide(VerticalAlignment.center, computeValue: { d in
// for simplicity of demo skipped checking for equality
DispatchQueue.main.async { // << must be async
self.width = d.width // << set limit
}
return d[VerticalAlignment.center]
})
}
HStack {
CellThreeView {
}
CellFourView {
}
.frame(width: self.width) // << apply limit, updated right in next loop
}