SwiftUI, is there a traitCollectionDidChange-like method in a View? - swiftui

I'm researching SwiftUI to see if it fits our compony's use case. However, I find it rather bare bones and wonder wether I am missing some key insights.
Currently I am researching if V/HStack's can respond dynamically to Accesability font-size changes.
The issue is that I need to give the items a frame, it does not automatically pin the outer Stacks items-view constraints to their inner content. When the user sets the font-size to full blown Bananas-mode the UI just breaks.
struct PatientHistoryView: View {
#State var model : PatientHistoryModel = historyModel;
var body : some View {
GeometryReader { g in
ScrollView(.vertical, showsIndicators: true) {
List
{
ForEach(self.model.data)
{ labResults in
LabDetailView(labeResults: labResults)
}
}.frame(width: 300, height: g.size.height).background(Color.clear)
}
}
}
}
adding these properties do not seem to affect the Font-scaling issues:
struct PatientHistoryView: View {
#State var model : PatientHistoryModel = historyModel;
var body : some View {
GeometryReader { g in
ScrollView(.vertical, showsIndicators: true) {
List
{
ForEach(self.model.data)
{ labResults in
LabDetailView(labeResults: labResults).frame(minWidth: 200, idealWidth: 400, maxWidth: 600, minHeight: 50, idealHeight: 100, maxHeight: 600)
}
}.frame(width: 300, height: g.size.height).background(Color.clear)
}
}
}
}
Here is the labDetailView
struct LabDetailView: View {
var labeResults : LabResults
var color : Color = .red
var spacer : some View {
Spacer().frame(width: 10, height: 0)
}
var body : some View {
GeometryReader { g in
HStack
{
Group
{
VerticalLineView().frame(width: 10, height: g.size.height, alignment: .leading).background(self.color)
self.spacer
self.spacer
LineStack(lineColor: .white, lineHeight: 2).frame(width: 20, height: 20, alignment: .center)
self.spacer
self.spacer
}
VStack(alignment: .leading, spacing: 8)
{
Text(self.labeResults.title).fontWeight(.bold)
Text(self.labeResults.type)
}
Spacer()
VStack(alignment: .center)
{
Text(self.labeResults.max.asString)
Spacer()
Text(self.labeResults.avg.asString).fontWeight(.bold).font(.system(size: 20))
Spacer()
Text(self.labeResults.min.asString)
}.frame(width: 50, height: g.size.height, alignment: .center)
self.spacer
self.spacer
}.frame(width: g.size.width, height: g.size.height, alignment: .leading)
}
}
}
In UIkit these issues can be resolved in the traitCollectionDidChange hook. However, in Swift UI I just cannot find an equivalent.
Doe anybody have any ideas?
Thanks!

Related

RoundedRectangle background colour does not crop and TextEditor transparent background

I have a messaging interface. When user types in to the texteditor it will be append to messagesDBArray and will be displayed in textview. Once new messages are there it should scroll to the bottom. But I'm having issues.
Errors: no errors
RoundedRectangle background colour green overflows from corners (does not crop as rounded)
TextEditor (not textview) is not transparent (so it can have rounded rectangle color underneath)
proxy.scrollTo(id, anchor: .bottom) does not scrolls to the last message.
import SwiftUI
final class ViewModel: ObservableObject {
#Published var messagesDBArray : [SingleMessageBubbleModel] = []
}
struct SingleMessageBubbleModel: Identifiable {
let id = UUID()
var text: String
var received: Bool
var timeStamp: Date
}
var messagesDBArray : [SingleMessageBubbleModel] = []
struct ContentView: View {
#ObservedObject private var messageArrayObservedObject = ViewModel()
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String = ""
var body: some View {
VStack (alignment: .center) {
ZStack (alignment: .center) {
HStack () {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.stroke(Color.brown, lineWidth: 1)
.frame(width: 300, alignment: Alignment.top )
.padding([.bottom], 5)
.clipped()
.background(Color.green)
}
HStack () {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(
messageArrayObservedObject.messagesDBArray,
id: \.id
) {
message in MessageBubble(message: message)
}
}
}
.frame(alignment: .center)
.background(Color.clear)
.padding (.vertical, 5)
.padding (.horizontal,5)
.padding(.bottom, 5)
.onChange(
of: messageArrayObservedObject.messagesDBArray.count
) { id in
// When the lastMessageId changes, scroll to the bottom of the conversation
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .center)
}
.frame(width: 295, alignment: Alignment.center )
}
HStack () {
VStack {
ZStack (alignment: .center) {
HStack () {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.stroke(Color.brown , lineWidth: 1)
.frame(width: 295, alignment: Alignment.top )
.padding([.bottom], 5)
.clipped()
.background(Color.green)
// .background(Color("#E5F2E4"))
}
HStack () {
TextEditor (text: $textTyped)
.frame(height: 200, alignment: .leading)
.padding(.horizontal, 10)
.background(.clear)
}
}
.frame(width: 290, alignment: Alignment.top )
.padding(.top, 5)
}
}
}
}
struct MessageBubble: View {
var message: SingleMessageBubbleModel
#State private var showTime = false
var body: some View {
VStack(alignment: message.received ? .leading : .trailing) {
HStack {
Text(message.text)
.padding()
.background(message.received ? Color.gray : Color.blue)
.cornerRadius(30)
}
.frame(maxWidth: 300, alignment: message.received ? .leading : .trailing)
.onTapGesture {
withAnimation {
showTime.toggle()
}
}
}
.frame(maxWidth: .infinity, alignment: message.received ? .leading : .trailing)
.padding(message.received ? .leading : .trailing)
.padding(.horizontal, 4)
}
}
for the first error you should use that code instead of your code where you make a background with RoundRectangle the same to your base rectangle and make the fill of that the color you want which is green
RoundedRectangle(cornerRadius: 25, style: .continuous)
.stroke(Color.brown, lineWidth: 1).background(RoundedRectangle(cornerRadius: 25).fill(Color.green))
.frame(width: 300, alignment: Alignment.top )
.padding([.bottom], 5)
.clipped()
the second issue in your ContentView you should init your UITextView background color to clear and after that make your textEditor Color clear using that code
init() {
UITextView.appearance().backgroundColor = .clear
}
and make your textEditor background clear
TextEditor (text: $textTyped)
.frame(height: 200, alignment: .leading)
.padding(.horizontal, 10)
.background(Color.clear)
and the third issue is that i think you are using the array count but you should use the id of each message so when if we suppose that the last message-id is 728398 in your onChange
onChange(of: messageArrayObservedObject.messagesDBArray.count) { id in
// When the lastMessageId changes, scroll to the bottom of the conversation
withAnimation {
print("ididididid\(id)")
proxy.scrollTo(messageArrayObservedObject.messagesDBArray.last, anchor: .bottom)
}
}
your are using the ( messageArrayObservedObject.messagesDBArray.count )counts of messages like 5 message so you are scrolling to 5 not to the id of message which is 728398

At SwiftUI how do you assign different actions if buttons are created in a loop

How can you assign different actions if you are creating buttons in a loop when using SwiftUI.
I was using sender tag while using UIView.
At following code, every buttons calls the same function as usual.
How can I make them call different action in case we are using loops to create buttons.
var buttonNames = ["OK", "NOPE"]
var body: some View {
let width = UIScreen.main.bounds.width / CGFloat(buttonNames.count)
ScrollView (.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(0..<buttonNames.count, id: \.self) { index in
Button(buttonNames[index]) {
buttonTouched()
}.frame(width: width)
.background(Color.gray)
}
}
.frame(width: UIScreen.main.bounds.width, height: 45, alignment: .center)
.foregroundColor(.white)
.background(Color.gray)
}
.position(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height - 45)
}
After the comment I did it like this and it makes the trick.
var buttonNames = ["OK", "NOPE"]
#State var touchedButton = 0
func buttonTouched(){
print("Button tapped! \(touchedButton)")
}
var body: some View {
let width = UIScreen.main.bounds.width / CGFloat(buttonNames.count)
ScrollView (.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(0..<buttonNames.count, id: \.self) {
index in
Button(action:{
touchedButton = index
buttonTouched()
}
){
Text(buttonNames[index])
}
.frame(width: width)
}
}
.frame(width: UIScreen.main.bounds.width, height: 45, alignment: .center)
.foregroundColor(.white)
.background(Color.gray)
} .position(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height - 45)
}

Spacing between sections in the same LazyVGrid?

Might be a dumb question and I might have already answered this myself. But, I am trying to put a space between my sections of grids. Currently I am using .padding but this does not seem ideal at all. Or is this really the only way?
var body: some View {
ScrollView(.vertical) {
LazyVGrid(
columns: columns,
alignment: .center,
spacing: 3,
pinnedViews: [.sectionHeaders, .sectionFooters]) {
Section(header:Text("GRID 1").font(.title).bold().padding(.top, 10.0)) {
ForEach(0...9, id: \.self) {
index in Color(UIColor.random)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 75, maxHeight: .infinity)
}
}
Section(header:Text("GRID 2").font(.title).bold().padding(.top, 50.0)) {
ForEach(10...20, id: \.self) {
index in Color(UIColor.random)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 75, maxHeight: .infinity)
}
}
}
}
}
If I understand your question correctly, one way to add vertical space between sections in a grid is to add a spacer with an explicit length (e.g. Spacer(minLength: 45)) inside the grid.
struct ContentView: View {
let gridItemLayout = [GridItem(.adaptive(minimum: 50, maximum: 50))]
var body: some View {
List {
LazyVGrid(columns: gridItemLayout, spacing: 25) {
SectionView()
Spacer(minLength: 45) <-------- VERTICAL SPACE BETWEEN SECTIONS
SectionView()
}
}
}
}
struct SectionView: View {
var body: some View {
Section() {
Color.red.frame(height: 50)
Color.black.frame(height: 50)
Color.green.frame(height: 50)
Color.blue.frame(height: 50)
Color.orange.frame(height: 50)
Color.pink.frame(height: 50)
Color.gray.frame(height: 50)
}
}
}

How I can have transform animation in SwiftUI?

Here I got 2 shapes, one Rectangle and one Circle, which with action of a Button only one of them became visible to user, I tried to use #Namespace for this transform, but did not panned out!
MY Goal: Having a nice and smooth transform animation from one Shape to other.
struct ContentView: View {
#State var action: Bool = false
#Namespace var sameShape
var body: some View {
ZStack
{
Group
{
if action
{
Circle()
.fill(Color.blue).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "Dakota148Zero", in: sameShape)
}
else
{
Rectangle()
.fill(Color.red).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "Dakota148Zero", in: sameShape)
}
}
.animation(.easeInOut)
VStack
{
Spacer()
Button("transform") { action.toggle() }.font(Font.largeTitle).padding()
}
}
}
}
Here is way to do it by representing the circle and square as a RoundedRectangle with different cornerRadius values:
struct ContentView: View {
#State var action = false
var body: some View {
ZStack
{
RoundedRectangle(cornerRadius: action ? 0 : 75)
.fill(action ? Color.red : .blue)
.frame(width: 150, height: 150, alignment: .center)
.animation(.easeInOut)
VStack
{
Spacer()
Button("transform") {
action.toggle()
}
.font(Font.largeTitle)
.padding()
}
}
}
}
Group is not real container, so don't store animation. Replace Group with some stack, like
VStack
{
if action
// ... other code no change
With iOS14 out, you can use matchedGeometryEffect(). If you are using iOS14, I would recommend this approach.
https://www.hackingwithswift.com/quick-start/swiftui/how-to-synchronize-animations-from-one-view-to-another-with-matchedgeometryeffect
https://developer.apple.com/documentation/swiftui/view/matchedgeometryeffect(id:in:properties:anchor:issource:)
So in your solution, if you replace action.toggle() with withAnimation{self.action.toggle()} in your button code, it will animate.
Button("transform") {
withAnimation{self.action.toggle()}
}
.font(Font.largeTitle).padding()
This solution works on the simulator for me (Xcode 12.1, iPhone 11 iOS 14.1):
import SwiftUI
struct ContentView: View {
#State var action: Bool = false
#Namespace var transition
var body: some View {
ZStack {
Group {
if action {
Circle()
.fill(Color.blue).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "shape", in: transition)
} else {
Rectangle()
.fill(Color.red).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "shape", in: transition)
}
}
.animation(.easeInOut)
VStack {
Spacer()
Button("transform") { withAnimation{self.action.toggle()} }.font(Font.largeTitle).padding()
}
}
}
}
The matchedGeometryEffect() doesn't want to animate different shapes (including cornerRadius) or colors that nicely, not sure if this is a bug that will get fixed in future patches or just a feature that needs to be worked around by regular animations. With me playing around with matchedGeometryEffect(), it seems to do great with sizing things up and down, like shown with this code:
import SwiftUI
struct ContentView: View {
#State private var animate: Bool = false
#Namespace private var transition
var body: some View {
VStack {
if animate {
RoundedRectangle(cornerRadius: 75.0)
.matchedGeometryEffect(id: "shape", in: transition)
.frame(width: 250, height: 250, alignment: .center)
.foregroundColor(Color.blue)
.animation(.easeInOut)
.onTapGesture {
animate.toggle()
}
} else {
// Circle
RoundedRectangle(cornerRadius: 75.0)
.matchedGeometryEffect(id: "shape", in: transition)
.frame(width: 150, height: 150, alignment: .center)
.foregroundColor(Color.red)
.animation(.easeInOut)
.onTapGesture {
animate.toggle()
}
}
}
}
}

How to ensure view appears above other views when iterating with ForEach in SwiftUI?

I have a SwiftUI view that is a circular view which when tapped opens up and is supposed to extend over the UI to its right. How can I make sure that it will appear atop the other ui? The other UI elements were created using a ForEach loop. I tried zindex but it doesn't do the trick. What am I missing?
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(Array(zip(1..., dataPoints)), id: \.1.id) { number, point in
VStack(alignment: .center, spacing: 5) {
DataCircle().zIndex(10)
ChartBar(percentage: point.percentage).zIndex(-1)
Text(point.month)
.font(.caption)
}
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
.offset(x: 30, y: 20)
}
.frame(width: 500, height: 300, alignment: .center)
}
}
}
}
.zIndex have effect for views within one container. So to solve your case, as I assume expanded DataCircle on click, you need to increase zIndex of entire bar VStack per that click by introducing some kind of handling selection.
Here is simplified replicated demo to show the effect
struct TestBarZIndex: View {
#State private var selection: Int? = nil
var body: some View {
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
Rectangle().fill(Color.yellow)//ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(1...10) { number in
VStack(spacing: 5) {
Spacer()
ZStack() { // DataCircle()
Circle().fill(Color.pink).frame(width: 20, height: 20)
.onTapGesture { self.selection = number }
if number == self.selection {
Text("Top Description").fixedSize()
}
}
Rectangle().fill(Color.green) // ChartBar()
.frame(width: 20, height: CGFloat(Int.random(in: 40...150)))
Text("Jun")
.font(.caption)
}.zIndex(number == self.selection ? 1 : 0) // << here !!
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
}
.frame(height: 300)
}
}
}
}