SwiftUI Fill HStack to parent view but not to full screen - swiftui

I want to make a chat-bubble. The content should be a text, the date and a name (here TextBla).
I want that the name is on the right side of the bubble. The time stays on the left side.
When I add a Spacer to the HStack the bubble fills the complete screen, but I want that the bubble is as width as the text is.
Here is my try:
HStack {
VStack(alignment: .leading) {
HStack {
Text(self.message.formattedTimeString)
.foregroundColor(self.textColor)
.font(Font.mcCaption)
Text(self.message.fromPlayer.name)
.foregroundColor(self.textColor)
.font(Font.mcCaption)
}
Text(self.message.message)
.font(Font.mcBody)
.layoutPriority(2)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(self.textColor)
}
}

Here is possible approach. Tested with Xcode 11.4. / iOS 13.4
Bubble element is
struct BubbleView: View {
let name: String
let time: String
let text: String
var color: Color = Color.gray.opacity(0.4)
var body: some View {
ZStack(alignment: .topTrailing) {
Text(name)
VStack(alignment: .leading) {
// 2nd needed to avoid overlap on very short text
Text(time) + Text("\t\(name)").foregroundColor(Color.clear)
Text(text)
}
}
.padding(8)
.background(RoundedRectangle(cornerRadius: 12).fill(color))
}
}
Demo code:
struct ContentView: View {
var body: some View {
ScrollView {
HStack {
ZStack(alignment: .topTrailing) {
Text("Name1")
VStack(alignment: .leading) {
Text("9:38")
Text("Lorem ipsum dolor sit amet")
}
}
.padding(8)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink.opacity(0.4)))
Spacer()
}
HStack {
ZStack(alignment: .topTrailing) {
Text("Name2")
VStack(alignment: .leading) {
Text("9:38")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut venenatis risus. Fusce eget orci quis odio lobortis hendrerit. Vivamus in sollicitudin arcu. Integer nisi eros, hendrerit eget mollis et, fringilla et libero.")
}
}
.padding(8)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink.opacity(0.4)))
Spacer()
}
HStack {
Spacer()
ZStack(alignment: .topTrailing) {
Text("Name3")
VStack(alignment: .leading) {
Text("9:38")
Text("Lorem ipsum dolor sit amet")
}
}
.padding(8)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.blue.opacity(0.4)))
}
}.padding(8)
}
}

try this:
struct ContentView: View {
var body: some View {
VStack {
HStack() {
VStack(alignment: .leading) {
HStack {
Text("9:38")
Spacer()
Text("Alfredo")
}
Text("important message")
.layoutPriority(2)
// .fixedSize(horizontal: true, vertical: true)
}.fixedSize()
.padding()
Spacer()
}
HStack() {
VStack(alignment: .leading) {
HStack {
Text("9:38")
Spacer()
Text("Alfredo")
}
Text("important dd important df important message important message important message important message dfd message important message important message important message important message important message ")
.lineLimit(50)
.layoutPriority(2)
.frame(width: UIScreen.main.bounds.width - 40)
// .fixedSize(horizontal: true, vertical: true)
}.fixedSize()
.padding()
Spacer()
}
}
}
}

Related

Keep VStack in center of the screen in SwiftUI

I'm new on SwiftUI and I don't know how to manage my views.
I have this code:
import SwiftUI
struct ContentView: View {
#State private var email: String = ""
#State private var passord: String = ""
var body: some View {
ZStack {
Image("corner")
.resizable()
.scaledToFill()
VStack {
VStack { //VStack1
TextField("Email", text: $email)
.frame(width: 300, height: 40)
.textFieldStyle(.roundedBorder)
.bold(true)
SecureField("Password", text: $passord)
.frame(width: 300, height: 40)
.textFieldStyle(.roundedBorder)
.bold(true)
Button {
//Do something
} label: {
Text("Forgot your password ?")
.underline()
.foregroundColor(.white)
}
}
VStack { // VStack2
Text("Not registered ?")
.font(.title2)
.foregroundColor(.white)
Button("Sign up") {}
.frame(width: 200, height: 37)
.foregroundColor(.white)
.background(Color(.orange))
.cornerRadius(20)
}
}
}
}
}
I want to place the VStack2 in the bottom of the screen and keep the VStack1 on the center of the screen.
How I can do that. I've try to search but I don't find the solution on StackOverflow.
I tried to play with Spacer() and padding() but I have not a good result.
Screen
A simple way to do this would be to add VStack1 to the ZStack which will place it in the centre of the screen. Then add wrap VStack2 in another VStack with a Spacer to push it to the bottom of the screen, e.g.
ZStack {
Image("corner")
VStack { //VStack1
// etc
}
VStack {
Spacer()
VStack { // VStack2
// etc
}
}
}
Simple put
HStack {
Spacer()
VStack {
Text("Centered")
}
Spacer()
}
import SwiftUI
struct ContentView: View {
#State private var email: String = ""
#State private var passord: String = ""
var body: some View {
VStack {
Spacer()
middleView()
Spacer()
bottomView()
.paddind(.bottom, 12)
}
.ignoresSafeArea()
.background {
Image("corner")
.resizable()
.scaledToFill()
}
}
func middleView() -> some View {
VStack(spacing: 20) {
TextField("Email", text: $email)
.frame(width: 300, height: 40)
.textFieldStyle(.roundedBorder)
.bold(true)
SecureField("Password", text: $passord)
.frame(width: 300, height: 40)
.textFieldStyle(.roundedBorder)
.bold(true)
Button {
//Do something
} label: {
Text("Forgot your password ?")
.underline()
.foregroundColor(.white)
}
}
}
func bottomView() -> some View() {
VStack {
Text("Not registered ?")
.font(.title2)
.foregroundColor(.white)
Button("Sign up") {}
.frame(width: 200, height: 37)
.foregroundColor(.white)
.background(Color(.orange))
.cornerRadius(20)
}
}
}

How to align element in different row in SwiftUI?

I want to create a list view with a text of name(which may be different length) and a text of version, and I want all text of version is aligned,how to realize it in SwiftUI?
Code of the rows
struct ComponentRow: View {
#EnvironmentObject var component: Component
var body: some View {
HStack {
Text(String(randomStringWithLength(len:Int(arc4random()%20+10))))
TextField("版本号", text: $component.version)
}
}
}
The list is
List {
ForEach(configure.components) { component in
ComponentRow().environmentObject(component)
}
}
You can use GeometryReader to get the full width and then give the single elements e.g. 2/3 and 1/3 of that space:
var body: some View {
List {
ForEach(0..<50) { component in
GeometryReader { geo in
HStack {
Text(title)
.lineLimit(1)
.padding(.trailing)
.frame(width: geo.size.width / 3 * 2, alignment: .leading)
TextField(number, text: $input) // dummy only!
.frame(width: geo.size.width / 3 * 1, alignment: .leading)
}
}
}
}
.listStyle(.plain)
.padding()
}
or use LazyVGrid, where you can also use other column sizes:
struct ContentView: View {
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
var body: some View {
List {
ForEach(0..<50) { component in
LazyVGrid(columns: columns, alignment: .leading) {
Text(title)
.lineLimit(1)
.padding(.trailing)
Text(number)
}
}
}
.listStyle(.plain)
.padding()
}
var title: String {
String("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec,".prefix(Int.random(in: 3..<30)))
}
var number: String {
String("1243568743584375872345734289758342759872435987".prefix(Int.random(in: 0..<10)))
}
}

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

SwiftUI Context Menu

My project was using a List but I needed a ScrollView wrapped view so I changed the List to just a ForEach loop with a view and Divider in between. I noticed my Context Menu doesn't look the same as when it was in the List as in the screenshot below.
I'd like the item selected with context menu to look like the List item selected since I can no longer use List in the code, it doesn't look as pretty. Also I have to press on the image and or text to open the Context Menu unlike in a List you can select the row. The end result would be to mimic the style of the selected List item.
/// Context Menu looks terrible
VStack (alignment: .leading, spacing: 0){
ForEach(self.welcome.views.dropLast(), id: \.id) { view in
ForEach(view.views ?? [], id: \.id) { purple in
ForEach(purple.views, id: \.id) { fluffy in
VStack (alignment: .leading, spacing: 0){
if viewClass == "FeaturedHeaderView" {
Text(title)
.font(.title3)
.fontWeight(.bold)
} else {
HStack (spacing: 15){
RequestImage(Url(image), animation: nil)
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.clipped()
.cornerRadius(13.5)
VStack (alignment: .leading){
Text(name)
.font(.headline)
.fontWeight(.bold)
Text(author)
.foregroundColor(Color.secondary)
.font(.footnote)
Text(repo)
.foregroundColor(Color.secondary)
.font(.caption)
}
}
.contextMenu(ContextMenu(menuItems: {
HStack {
Button(action: {
openURL(URL(string: "URL")!)
}) {
Label("Open in Safari", systemImage: "safari.fill")
}
}
}))
}
}
Divider()
}
}
}
.padding(.all, 5)
.padding(.leading, 5)
}
/// List Context Menu looks great but can't use List in ScrollView
List {
ForEach(self.welcome.views.dropLast(), id: \.id) { view in
ForEach(view.views ?? [], id: \.id) { purple in
ForEach(purple.views.dropLast(), id: \.id) { fluffy in
VStack (alignment: .leading, spacing: 0){
if viewClass == "FeaturedHeaderView" {
Text(title)
.font(.title3)
.fontWeight(.bold)
} else {
HStack (spacing: 15){
RequestImage(Url(image), animation: nil)
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.clipped()
.cornerRadius(13.5)
VStack (alignment: .leading){
Text(name)
.font(.headline)
.fontWeight(.bold)
Text(author)
.foregroundColor(Color.secondary)
.font(.footnote)
Text(repo)
.foregroundColor(Color.secondary)
.font(.caption)
}
}
.contextMenu(ContextMenu(menuItems: {
HStack {
Button(action: {
openURL(URL(string: "URL")!)
}) {
Label("Open in Safari", systemImage: "safari.fill")
}
}
}))
}
}
}
}
}
.padding(.all, 10)
.listRowInsets(EdgeInsets())
}
The possible solution is to add background modifier above contextMenu, like
.frame(maxWidth: .infinity) // << to make it screen-wide
.background(
RoundedRectangle(cornerRadius: 12) // << tune as needed
.fill(Color(UIColor.systemBackground)) // << fill with system color
)
.contextMenu(ContextMenu(menuItems: {
Demo with some replication (used Xcode 12.4 / iOS 14.4)

How to reproduce the height/stacking effect with a ZStack in SwiftUI

I would like to reproduce inside a ZStack the height effect that you see when a modal is presented in iOS 13:
The background is slightly transparent and becomes greyish with a blur effect.
The idea would be to create a view that seems to be floating above the elements below inside a ZStack.
You can have a ZStack with a view that is conditionally displayed depending on a #State variable. The variable will also determine the .blur amount on the underlying view and whether or not a transparent gray view is displayed in between (which makes the background look grayed out). I made an example to illustrate:
struct ContentView: View {
#State var modalIsPresented = false
#State var alertIsPresented = false
#State var customAlertIsPresented = false
var body: some View {
ZStack {
Text("Test!!!")
VStack {
Spacer()
Text("Lorem ipsum dolor sit amet")
Spacer()
Text("Lorem ipsum dolor sit amet")
Image(systemName: "star")
Button(action: {
self.modalIsPresented = true
}) {
Text("Present an actual modal")
}
Button(action: {
self.alertIsPresented = true
}) {
Text("Present an actual alert")
}
Button(action: {
withAnimation {
self.customAlertIsPresented = true
}
}) {
Text("Present your custom alert")
}
}
.blur(radius: self.customAlertIsPresented ? 3 : 0)
.animation(.easeOut)
if customAlertIsPresented {
Rectangle()
.background(Color.black)
.opacity(0.3)
.edgesIgnoringSafeArea(.all)
.animation(.easeIn)
}
if customAlertIsPresented {
CustomAlert(isPresented: $customAlertIsPresented).frame(width: 300)
.background(Color.white)
.animation(.easeIn)
.cornerRadius(10)
.shadow(radius: 10)
}
}.sheet(isPresented: $modalIsPresented) {
Text("This is what an actual modal looks like")
}.alert(isPresented: $alertIsPresented) {
Alert(title: Text("This is what an alert looks like"))
}
}
}
struct CustomAlert: View {
#Binding var isPresented: Bool
var body: some View {
VStack {
Text("This is my custom alert").padding()
Divider()
Button(action: {
self.isPresented = false
}) {
HStack {
Spacer()
Text("Dismiss")
Spacer()
}
}.padding([.top, .bottom], 10)
}
}
}
I added some animations to make the transition smoother. You can adjust things like the .blur, .opacity, and various .animations to customize it took look just the way you want. Here's what my example looks like: