How to align element in different row in SwiftUI? - 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)))
}
}

Related

SwiftUI : animate row expand in List

I need to expand each row of a List with a smooth animation (from top to bottom). A row has a main text (always displayed) and a secondary text that is shown only when row is expanded.
Here is code for List:
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach((1...3), id: \.self) { _ in
ItemRow()
}
}
.navigationTitle("Demo")
.navigationBarTitleDisplayMode(.inline)
}
}
}
And for ItemRow:
struct ItemRow: View {
#State private var expanded = false
var body: some View {
VStack {
HStack {
Text("Hello world")
Spacer()
Image(systemName: "chevron.forward.circle")
.rotationEffect(.degrees(expanded ? 90 : 0))
.onTapGesture {
withAnimation() {
expanded.toggle()
}
}
}
if expanded {
Text("Lorem ipsum dolor sit amet. Vel dicta error qui vero incidunt et fugit quisquam aut modi praesentium qui veritatis sed ipsam magnam. Ad iure velit ut possimus voluptatem cum dolores dicta. Ex cupiditate libero ut impedit internos aut reprehenderit molestias! Aut debitis dignissimos sit incidunt internos aut mollitia explicabo aut vitae numquam et repellendus iusto.")
.foregroundColor(.secondary)
.font(.caption)
.frame(height: expanded ? nil : 0)
.clipped()
}
}
}
}
Rows expand when I tap on chevron button. But the animation is not smooth at all: the main text starts in center at first and moves on top of row. Demo video
The goal is that main text stays on top (fixed) and the secondary text (and all row height) expands from top to bottom.
If I replace List in ContentView with ScrollView the animation is a bit better. But the UI does not look like a standard List anymore. And I need List features (like swipeActions).
Any idea how to make a great animation by keeping List?
The best solution is DisclosureGroup from Apple.
Just replace your ItemRow with the following:
struct ItemRow: View {
var body: some View {
DisclosureGroup {
Text("Lorem ipsum dolor sit amet. Vel dicta error qui vero incidunt et fugit quisquam aut modi praesentium qui veritatis sed ipsam magnam. Ad iure velit ut possimus voluptatem cum dolores dicta. Ex cupiditate libero ut impedit internos aut reprehenderit molestias! Aut debitis dignissimos sit incidunt internos aut mollitia explicabo aut vitae numquam et repellendus iusto.")
.foregroundColor(.secondary)
.font(.caption)
.clipped()
} label: {
Text("Hello world")
}
}
}
This is the animation you get:
Since DiscloureButtonStyle is iOS 16 Beta only, here is a trick to show custom chevron (I don't recommend this approach btw):
struct ItemRow: View {
#State private var expanded = false
var body: some View {
DisclosureGroup(isExpanded: $expanded) {
Text("Lorem ipsum dolor sit amet. Vel dicta error qui vero incidunt et fugit quisquam aut modi praesentium qui veritatis sed ipsam magnam. Ad iure velit ut possimus voluptatem cum dolores dicta. Ex cupiditate libero ut impedit internos aut reprehenderit molestias! Aut debitis dignissimos sit incidunt internos aut mollitia explicabo aut vitae numquam et repellendus iusto.")
.foregroundColor(.secondary)
.font(.caption)
.clipped()
} label: {
HStack {
Text("Hello world")
Spacer()
Image(systemName: "chevron.forward.circle")
.rotationEffect(.degrees(expanded ? 90 : 0))
}.padding(.trailing, -20) // <-- To use the space of the default one
}.accentColor(.clear) // <-- To hide the default one
}
}

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 Fill HStack to parent view but not to full screen

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()
}
}
}
}