Fixing overlapping views in SwiftUI - swiftui

I am trying to build a screen similar to the following.
But there are few things that are wrong with this screen:
The image and the text below it are not exactly the same width. The image is slightly wider.
The image and its associated text overlap the text above it.
The text underneath the image is truncated.
I've been on this for two days with no luck! Can someone provide feedback on how I can achieve this? Below is what I have in terms of code.
struct ImageTitleTileView: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
Image("image")
.resizable()
.frame(width: geometry.size.width)
.aspectRatio(1, contentMode: .fill)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.")
.padding()
.background(Color.white)
}
.edgesIgnoringSafeArea(.all)
}.aspectRatio(contentMode: .fit)
}
}
struct MainItemView: View {
var body: some View {
ZStack {
Color(.whiteSmoke)
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.")
ImageTitleTileView()
}.padding(16)
}
}
}

What its happening is that your image is showing the image part outside your frame.
You need to set clipped() property to the image to hide the image parts outside your frame.
Image("image")
.resizable() // allow image to be resizable
.scaledToFill() // scale your image as you need it to fill or to fit
.frame(height: 200, alignment: .center) // set image frame size
.aspectRatio(contentMode: .fill) // image aspect ratio
.clipped() // hide image outside the frame parts (CSS: overflow: hidden)

Just change some parameters . You can get this work.
One is the image ratio from 1.0 to 0.9;
The other is moving the frame from image to Stack;
The last is removing the padding from the 'text'
struct ImageTitleTileView: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
Image("image")
.resizable()
.aspectRatio(0.90, contentMode: .fill)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.")
.background(Color.white)
}.frame(width: geometry.size.width / 1.5)
.edgesIgnoringSafeArea(.all)
}.aspectRatio(contentMode: .fit)
}
}

Related

Cannot place SwiftUI view outside the SafeArea when embedded in UIHostingController

I have a simple SwiftUI view that contains 3 text elements:
struct ImageDescriptionView: View {
var title: String?
var imageDescription: String?
var copyright: String?
var body: some View {
VStack(alignment: .leading) {
if let title = title {
Text(title)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let imageDescription = imageDescription {
Text(imageDescription)
.foregroundColor(.white)
.fontWeight(.medium)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let copyright = copyright {
Text(copyright)
.font(.body)
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.background(
Color.blue
)
}
}
The SwiftUI View is embedded within a UIHostingController:
class ViewController: UIViewController {
private var hostingController = UIHostingController(rootView: ImageDescriptionView(title: "25. November 2021", imageDescription: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", copyright: "Bild © Unknown"))
override func viewDidLoad() {
super.viewDidLoad()
setUpHC()
}
private func setUpHC() {
hostingController.view.backgroundColor = .red
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
addChild(hostingController)
hostingController.didMove(toParent: self)
}
}
The result looks like this:
The UIHostingController is always bigger than the view. Also, it will always make the SwiftUI view respect the safe area (which in my case, I do not want)
The look I want:
(please don't comment the usability of the home indicator, that's not the case here)
What's the problem with UIHostingController? I tried setting .edgesIgnoreSafeArea(.all) on all Views within ImageDescriptionView, did not help.
On the UIHostingControllers property try the following
viewController._disableSafeArea = true
that should do the trick.
Got a discussion here, and the detail here
extension UIHostingController {
convenience public init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: #convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
I came across the same issue. You have to ignore the safe area at the SwiftUI view level.
var body: some View {
VStack(alignment: .leading) {
...
}
.ignoresSafeArea(edges: .all) // ignore all safe area insets
}
Happened to me too. When I aligned my view with the frame it worked, but to make it work with autolayout I had to consider the height of the safe area to make it work with UIHostingController, even though I didn't have to do that with a standard view.
code:
hostingVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: view.safeAreaInsets.bottom).isActive = true

How to get the TextEditor to display multiple lines?

I wanted the TextEditor to display eg. three lines of text. I have some sample code like this:
import SwiftUI
struct MultiLineText: View {
#State var value: String
#State var text: String
var body: some View {
Form {
TextField("Title", text: $value)
TextEditor(text: $text)
}
}
}
struct MultiLineText_Previews: PreviewProvider {
static var previews: some View {
MultiLineText(value: "my title", text: "some text")
}
}
The problem is, that I always see only one line at a time for both controls (TextEditor and TextField) although I would like to have multiple lines displayed for the TextEditor.
How to realise this?
This is how Form works. The possible (simple) solution is to give TextEditor a frame
TextEditor(text: $text)
.frame(height: 80)
Update: more complicated case (dynamic calculation based on reference text, and taking into account dynamic font size)
Default
[
Large font settings
[
let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
struct ContentView: View {
#State var value: String = lorem
#State var text: String = lorem
#State private var textHeight = CGFloat.zero
var body: some View {
Form {
TextField("Title", text: $value)
TextEditor(text: $text)
.frame(minHeight: textHeight)
}
.background(
Text("0\n0\n0") // any stub 3 line text for reference
.padding(.vertical, 6) // TextEditor has default inset
.foregroundColor(.clear)
.lineLimit(3)
.background(GeometryReader {
Color.clear
.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
})
)
.onPreferenceChange(ViewHeightKey.self) { self.textHeight = $0 }
}
}
The ViewHeightKey preference is taken from this my answer
Note: in-code fonts for reference Text and TextEditor should be used the same

TextEditor sticking to minHeight inSwiftUI

I am trying to build a view with the newly introduced TextEditor. The idea is that I have some content at the top (blue frame), then a ScrollView with a TextEditor and a variable number of Text below it (red frame).
The TextEditor(yellow frame) view is supposed to have a minimum height, but should take up all the available space if there aren't to many Text views following – which it currently does not do...
import SwiftUI
struct ScrollViewWithTextEditor: View {
var comments = ["Foo", "Bar", "Buzz"]
var loremIpsum = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"""
var body: some View {
VStack {
Group {
Text("Some Content above")
}
.frame(maxWidth: .infinity)
.border(Color.blue, width: 3.0)
.padding(.all, 10)
ScrollView {
ScrollView {
TextEditor(text: .constant(loremIpsum))
.frame(minHeight: 200.0)
}
.frame(minHeight: 200.0)
.border(Color.yellow, width: 3.0)
.cornerRadius(3.0)
.padding(.all, 10.0)
VStack {
ForEach(comments, id: \.self) { comment in
Text(comment)
}
.padding(.all, 10)
.frame(maxWidth: .infinity, alignment: .leading)
.border(Color.gray, width: 1)
.cornerRadius(3.0)
.padding(.all, 10)
}
}
.frame(minHeight: 200.0)
.border(Color.red, width: 3)
.padding(.all, 3)
}
}
}
struct ScrollViewWithTextEditor_Previews: PreviewProvider {
static var previews: some View {
ScrollViewWithTextEditor()
}
}
Any suggestions on how to solve this?
Here is possible solution. Tested with Xcode 12 / iOS 14.
ScrollView {
// make clear static text in background to define size and
// have TextEditor in front with same text fit
Text(loremIpsum).foregroundColor(.clear).padding(8)
.frame(maxWidth: .infinity)
.overlay(
TextEditor(text: .constant(loremIpsum))
)
}
.frame(minHeight: 200.0)
.border(Color.yellow, width: 3.0)

SwiftUI views in VStack are overlapping each other

I have two views in a VStack. All looks fine until I try to enlarge the font in accessibility setting. Then for some reason the stack is not expanding to accommodate both views. Instead it is pushing one view on top of another. See below.
How can I align these correctly? Below is my code.
var body: some View {
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.")
.fixedSize(horizontal: false, vertical: true)
.padding()
GeometryReader { geometry in
VStack(spacing: 0) {
Image("tmp")
.resizable()
.scaledToFill()
.frame(width: geometry.size.width * 0.88)
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
.frame(width: geometry.size.width * 0.8, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
.frame(width: geometry.size.width * 0.8, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
.frame(width: geometry.size.width * 0.8, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
}
.padding()
.background(
Rectangle()
.fill(Color.white)
)
}
.cornerRadius(10)
.edgesIgnoringSafeArea(.all)
}
.scaledToFit()
.shadow(color: .gray, radius: 10, x: 5, y: 5)
.scaledToFill()
Spacer()
}
.background(Rectangle()
.fill(Color.gray)
.scaledToFill())
}
The Issue:
The issue of overlapping is with this section:
.scaledToFit() // Not needed
.shadow(color: .gray, radius: 10, x: 5, y: 5)
.scaledToFill() // Not needed
The Answer:
You don't need neither scaledToFit nor scaledToFill there.
Visualizing the Issue:
using scaledToFill:
using scaledToFit:
See blue borders? Thats the issue.
Some Tips:
There is no need to create a dummy Rectangle for background color. .background modifier can accept a color directly like:
.background(Color.gray)
You can ignore safe area only for background modifier like:
.background(Color.gray.edgesIgnoringSafeArea(.all))
Don't repeat modifiers! group them together and apply once like:
Group {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
}
.frame(width: geometry.size.width * 0.8, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
Multiplying width in a float number can cause misaligned images. So always round the result like:
(geometry.size.width * 0.88).rounded(.down)
Result:
Image:
Refactored Code:
var body: some View {
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.")
.padding()
GeometryReader { geometry in
VStack(spacing: 0) {
Image("tmp")
.resizable()
.scaledToFill()
.frame(width: (geometry.size.width * 0.88).rounded(.down))
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
}
.frame(width: (geometry.size.width * 0.8).rounded(.down), alignment: .leading)
.padding()
.background(Color.white)
}
.cornerRadius(10)
}
.shadow(color: .gray, radius: 10, x: 5, y: 5)
Spacer()
}
.background(Color.gray.edgesIgnoringSafeArea(.all))
}
Try changing the modifiers on the image to the following.
Image("tmp")
.resizable()
.scaledToFit()
.frame(maxWidth: geometry.size.width * 0.88)

Multiline Text does not work in a NavigationLink inside of a List in SwiftUI

Multiline Text in a NavigationLink inside of a List does not seem to work.
Here is the code:
struct ContentView : View {
var body: some View {
List(1...5) { _ in
NavigationLink(destination: EmptyView()) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
.lineLimit(nil)
}
}
}
}
Removing the NavigationLink, the Text behaves as expected.
Is there a way to fix this, or is this a bug?
UPDATE
It seems Beta 5 has solved this bug!
Workaround for Beta 4 and previous versions:
It seems NavigationLink is "broken". But you can use DynamicNavigationDestinationLink instead. I know it's too verbose, but if you need a way out, here you have it. At least until NavigationLink works better.
struct ContentView: View {
var body: some View {
NavigationView {
TopView().navigationBarTitle(Text("Top View"))
}
}
}
struct TopView: View {
let detailView = DynamicNavigationDestinationLink(id: \String.self) { data in
DetailView(passedData: data)
}
var body: some View {
List(1...5) { i in
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
.lineLimit(nil)
.tapAction { self.detailView.presentedData?.value = "Detail for Row #\(i)" }
}
}
}
struct DetailView: View {
let passedData: String
var body: some View {
Text(passedData)
}
}
In the current XCode 13.1 Beta I still have an issue with this when the Text view is inside a container view. I could solve this giving the row (the container) a minimum height:
Text("blabla").lineLimit(2).frame(minHeight: 50)