Update UIViewRepresentable size from UIKit in SwiftUI - swiftui

I'm embedding a view controller with variable-height UITextView inside a parent SwiftUI VStack and the view controller sizes it's frame to the whole screen between viewDidLoad and viewDidLayoutSubviews. The UITextView expands only to the size of the text inside itself and centers itself inside the parent view.
I'm trying to add this view controller in a VStack and have it behave externally like other SwiftUI components do - sized exactly to the content it contains - but it wants to be sized to the whole screen minus the other VStack elements.
I can get the correct size of the UITextView in didLayoutSubviews and pass it upwards to SwiftUI where it can be set properly - but where do I do that?
In the example screenshot below, the orange is the embedded UIView background, the green is the UITextView and the VStack looks like this:
VStack {
HighligherVC()
Text("Tap and drag to highlight")
.foregroundColor(.gray)
.font(.caption)
}

Without being able to see more of your code, it's slightly difficult to say what the best solution would be, but based purely on this part of your question...
I can get the correct size of the UITextView in didLayoutSubviews and pass it upwards to SwiftUI where it can be set properly - but where do I do that?
I would suggest that you pass a binding property to your view controller that can be set to the calculated text view height, meaning that the view that contains your VStack would have a #State property like this:
#State private var textViewHeight: CGFloat = 0
You would then declare a #Binding property on your HighlighterVC and add an initializer like this:
#Binding var textViewHeight: CGFloat
init(textViewHeight: Binding<CGFloat>) {
self._textViewHeight = textViewHeight
}
And then you would set textViewHeight to the calculated height in your didLayoutSubviews and add a .frame modifier to your HighlighterVC like this:
VStack {
HighlighterVC(textViewHeight: self.$textViewHeight)
.frame(height: self.textViewHeight)
Text("Tap and drag to highlight")
.foregroundColor(.gray)
.font(.caption)
}
Like I said at the beginning of my answer, this solution (that I believe would work, but since I can't test it, I'm not 100% certain) is based on your thoughts about what it is that you need. Without seeing more code, it's impossible for me to say if this is the best solution.

Add fixedSize may solve this.
.fixedSize(horizontal: false, vertical: true)

Related

Why is the SwiftUI TextEditor glitching upon font size changes?

I'm trying to create a TextEditor view where the user can customize the font size, but whenever I adjust the font size using a Slider and go back to typing in the TextEditor, the TextEditor starts glitching: the scroll position jumps up and down, the cursor ends up in the wrong position, and the text flickers.
Here's a reduced test case:
struct ContentView: View {
#State var text: String
#State var textSize: CGFloat
var body: some View {
VStack {
Slider(value: $textSize, in: 20.0...80.0)
TextEditor(text: $text)
.font(.system(size: textSize, weight: .semibold))
}
.padding()
}
}
Just type some text, adjust the font size, type more text. You might need to repeat that a few times, but it eventually starts glitching.
My hypothesis is that part of the UI isn't being updated on the main thread, but maybe that's just me coming from UIKit and not fully understanding SwiftUI yet.
Using Xcode 14.2 and iOS 16.2.

SwiftUI: how can I hide scroll indicators? [duplicate]

I want to create a SwiftUI List, but not show scroll indicators. ScrollView offers showsIndicators to do this. How can it be done?
Any Indicators (List, scrollView, etc.)
you can get rid of showing indicators for all Lists, but with an API of the UITableView. because SwiftUI List is using UITableView for iOS behind the scene:
struct ContentView: View {
init() {
UITableView.appearance().showsVerticalScrollIndicator = false
}
var body: some View {
List(0...100, id: \.self) { item in
Text("hey")
}
}
}
Note that this will eliminate all TableViews and Lists indicators. You should make it visible again if you need to.
⚠️ Not Yet Important Note
Seems like Apple is removing appearance hacks (but not for this one yet). So you can use LazyVStack inside and ScrollView instead of List and use the available argument for hiding the indicators.
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) { // <- This argument
LazyVStack {
ForEach(1...100, id: \.self) {
Text("\($0)").frame(height: 40)
}
}
}
}
}
It is actually easy to accomplish this without any appearance work arounds in the answer accepted. You just have to use the ScrollView initializer and set the parameter showsIndicators to false.
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}
Inside your ScrollView you could use LazyVStack, if you have a lot of subviews to scroll through. SwiftUI will then render very efficiently your subviews: "lazy" -> only if needed).
Until there is a native SwiftUI way to achieve this, the Introspect library provides a decent solution.
After applying all modifiers to your list just add as a last modifier the following:
List {
...
}
.introspectTableView { tableView in
tableView.showsVerticalScrollIndicator = false // here you can access any other UITableView property
}
I hope there is a native way to do that at some point.
Hide scrolling indicator now became very simple
List {}.scrollIndicators(ScrollIndicatorVisibility.hidden)
List basically creates a tableview (UpdateCoalescingTableView) behind the scenes, and tableview's are scrollable. Unfortunately, however, you can't get to the scrollview attributes in SwiftUI.
You "might" be able to create a UIViewRepresentable that could walk up the view hierarchy until it finds a scrollview, but I wouldn't recommend it.
You could also create your own scrollview, put a vstack inside it, and "fake" a list view, which would probably be the safer approach.
The choosen answer won't work in iOS 16. They released a new viewModifier called .scrollIndicators(.hidden). I created a viewModifier wrapper which you can call like this on your List: .modifier(HideListIndicatorsViewModifier())
struct HideListIndicatorsViewModifier: ViewModifier {
#ViewBuilder
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.scrollIndicators(.hidden)
} else {
content
}
}
}
swift scrollview hide scrollbar
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
hide scroll view indicators bar swiftui
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}

Project ARKit + SwiftUI + Different layout preview/simulator

I cannot understand why in the simulator the layout is different from the layout displayed in xcode/preview.
struct ContentView : View {
var body: some View {
ZStack(alignment: .bottom) {
ARViewContainer()
Text("hello")
}
}
}
here the screenshots:
TLDR; I don't know why your simulator and preview don't match, but I do know why it's appearing the way that it is on the device. Are you getting any errors in the debug?
Any container views in SwiftUI will only take up the required space that they need. they will also distribute according to your settings. For example, you have a ZStack that contains a bottom alignment. You also have a ARViewContainter() that takes up a portion of that stack. They are aligned behind each other on the Z axis where the text is in front and the other container is behind. A quick way to prove this and test it is to include a background shape behind everything for example.
var body: some View {
ZStack {
Rectangle().edgesIgnoringSafeArea(.all)
.foregroundColor(Color.red)
//Your Views
}
}
This will force the ZStack to take up all available space and your other views should then align as expected. Basically, your Text is aligning to the bottom of the maximum provided space, which is being provided by your ARViewContainer()
Further Reading and Understanding
Views only take up the space required for what's in them, unless otherwise specified.
ZStacks operate on the Z axis, forward/backwards.
In your case you have a view with a set size called ARViewContainer() which takes up the width of the screen and a portion of the height. Since it's the largest view you have, the ZStack inherits that size.
Your text is smaller than the ZStack so the ZStack does NOT inherit the size. You do however have a .bottom assignment. So your text is over your ARViewContainer() and aligned to the .bottom edge of that container.
Finally the ZStack is centered in the remaining space available, giving it the impression that your .bottom isn't doing anything, when in reality it is.
Reproducing the Issue
Here is a code snippet that reproduces your issue and makes it a bit clearer and easier to understand.
var body: some View {
ZStack(alignment: .bottom) {
Rectangle().foregroundColor(Color.yellow).frame(width: 100, height: 100, alignment: .center)
Text("Test")
}
}

How do I get my UIView to correctly size for its content in SwiftUI?

I want to use the awesome MultiSegmentPicker written by Yonat Sharon in my SwiftUI View.
https://github.com/yonat/MultiSelectSegmentedControl
However, I don't fully understand the interaction between the UIViewRepresentable View and my SwiftUI View. How do I get the host view controller to shrink its height down to the size of the segmented control?
Here's the debugger view of the demo page - notice the blue area around the top bar:
The demo code doesn't give a lot of insight into the issue, it's just a call to the UIViewRepresentable view. I've simplified it to just one example here:
struct MultiSegmentPickerX: View {
#State private var selectedSegmentIndexes: IndexSet = []
var body: some View {
VStack(alignment: .center) {
Spacer()
MultiSegmentPicker(
selectedSegmentIndexes: $selectedSegmentIndexes,
items: ["First", "Second", "Third", "Done"]
)
}
}
}
Notice I have a VStack with a Spacer() before the control.
The desired behavior for this example would be that the bar with "First", "Second", etc. would be snug against the bottom of the screen. Instead the Host Controller holds onto all that space...
Do I need to use Geometry reader to solve this issue and shrink the height down. Or do I have to adjust something in the UIViewRepresentable View?
Any insights into bridging UIKit and SwiftUI are always appreciated...
Is this an easy fix anyone?
These did not solve my issue:
UIViewRepresentable content not updating
How do I make SwiftUI UIViewRepresentable view hug its content?
How do I size a UITextView to its content?
Cannot test it now, just by thoughts, try with fixed size as below
MultiSegmentPicker(
selectedSegmentIndexes: $selectedSegmentIndexes,
items: ["First", "Second", "Third", "Done"]
).fixedSize() // << here !!

Is there a way to hide scroll indicators in a SwiftUI List?

I want to create a SwiftUI List, but not show scroll indicators. ScrollView offers showsIndicators to do this. How can it be done?
Any Indicators (List, scrollView, etc.)
you can get rid of showing indicators for all Lists, but with an API of the UITableView. because SwiftUI List is using UITableView for iOS behind the scene:
struct ContentView: View {
init() {
UITableView.appearance().showsVerticalScrollIndicator = false
}
var body: some View {
List(0...100, id: \.self) { item in
Text("hey")
}
}
}
Note that this will eliminate all TableViews and Lists indicators. You should make it visible again if you need to.
⚠️ Not Yet Important Note
Seems like Apple is removing appearance hacks (but not for this one yet). So you can use LazyVStack inside and ScrollView instead of List and use the available argument for hiding the indicators.
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) { // <- This argument
LazyVStack {
ForEach(1...100, id: \.self) {
Text("\($0)").frame(height: 40)
}
}
}
}
}
It is actually easy to accomplish this without any appearance work arounds in the answer accepted. You just have to use the ScrollView initializer and set the parameter showsIndicators to false.
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}
Inside your ScrollView you could use LazyVStack, if you have a lot of subviews to scroll through. SwiftUI will then render very efficiently your subviews: "lazy" -> only if needed).
Until there is a native SwiftUI way to achieve this, the Introspect library provides a decent solution.
After applying all modifiers to your list just add as a last modifier the following:
List {
...
}
.introspectTableView { tableView in
tableView.showsVerticalScrollIndicator = false // here you can access any other UITableView property
}
I hope there is a native way to do that at some point.
Hide scrolling indicator now became very simple
List {}.scrollIndicators(ScrollIndicatorVisibility.hidden)
List basically creates a tableview (UpdateCoalescingTableView) behind the scenes, and tableview's are scrollable. Unfortunately, however, you can't get to the scrollview attributes in SwiftUI.
You "might" be able to create a UIViewRepresentable that could walk up the view hierarchy until it finds a scrollview, but I wouldn't recommend it.
You could also create your own scrollview, put a vstack inside it, and "fake" a list view, which would probably be the safer approach.
The choosen answer won't work in iOS 16. They released a new viewModifier called .scrollIndicators(.hidden). I created a viewModifier wrapper which you can call like this on your List: .modifier(HideListIndicatorsViewModifier())
struct HideListIndicatorsViewModifier: ViewModifier {
#ViewBuilder
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.scrollIndicators(.hidden)
} else {
content
}
}
}
swift scrollview hide scrollbar
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
hide scroll view indicators bar swiftui
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}