SwiftUI ScrollView horizontal RTL not working correctly - uiscrollview

I'm trying to support RTL mode on my ios , which was built using swiftUI
every is fine while doing this to change the layour direction :
.environment(\.layoutDirection, .rightToLeft)
Only with horizontal ScrollView , it's not work correctly
when i do this :
ScrollView(.horizontal) {
HStack{
Text("b1")
Text("b2")
Text("b3")
}
} .environment(\.layoutDirection, .rightToLeft)
Items positions will rotate , but the HSTACK will stay always on the left like the screenshot below :
Any solution or hack to make it to the right ?

I find a hack to do this using .flipsForRightToLeftLayoutDirection(true) and .rotation3DEffect
Like that the scrollview will be flipped , than you need to flip each item of HStack to .rotation3DEffect
ScrollView(.horizontal) {
HStack{
Text("b1")
.rotation3DEffect(Angle(degrees: 180), axis: (x: CGFloat(0), y: CGFloat(10), z: CGFloat(0)))
Text("b2")
.rotation3DEffect(Angle(degrees: 180), axis: (x: CGFloat(0), y: CGFloat(10), z: CGFloat(0)))
Text("b3")
.rotation3DEffect(Angle(degrees: 180), axis: (x: CGFloat(0), y: CGFloat(10), z: CGFloat(0)))
}
}.flipsForRightToLeftLayoutDirection(true)
.environment(\.layoutDirection, .rightToLeft)

as a hack, you coud start with this:
var body: some View {
GeometryReader { geom in
ScrollView(.horizontal) {
HStack {
Text("b1")
Text("b2")
Text("b3")
}.environment(\.layoutDirection, .rightToLeft)
.padding(.leading, geom.size.width - 100) // adjust for the number of items
}
}
}

We had a lot of problems with our arabic related our application, the modifier below solved our problem, don't forget to follow me on githup.
https://gist.github.com/metin-atalay/bdf28a604fe9678403730d14f02946ca

By programatically scrolling (with .scrollTo) to trailing anchor of view in onAppear if layoutDirection is .rightToLeft, you get correct scroll position.
Works also if layoutDirection is .leftToRight
struct ScrollViewRTL<Content: View>: View {
var showsIndicators: Bool
#ViewBuilder var content: Content
#Environment(\.layoutDirection) private var layoutDirection
init(showsIndicators: Bool = true, #ViewBuilder content: () -> Content) {
self.showsIndicators = showsIndicators
self.content = content()
}
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: showsIndicators) {
content
.id("RTL")
.onAppear {
if layoutDirection == .rightToLeft {
proxy.scrollTo("RTL", anchor: .trailing)
}
}
}
}
}
}
Usage:
ScrollViewRTL {
HStack {
Text("b1")
Text("b2")
Text("b3")
}
}

Related

Simultaneous matchedGeometryEffect and rotation3DEffect

I want to animate a card, which flies from top half of screen to the bottom half and flips during the fly.
I control the flipping logic using custom .cardify modifier. It seems working alone, e.g. when I flip a card by onTapGesture { withAnimation { ... } }.
I also made a card fly from top of screen to the bottom and vice versa using matchedGeometryEffect.
But, when I tap card it flies without rotation.
I tried to apply .transition(.assymetric(...)) for both if-branches (see code below) but it did not help.
So, the code
import SwiftUI
struct ContentView: View {
#State var cardOnTop = true
#State var theCard = Card(isFaceUp: true)
#Namespace private var animationNameSpace
var body: some View {
VStack {
if cardOnTop {
CardView(card: theCard)
.matchedGeometryEffect(id: 1, in: animationNameSpace)
.onTapGesture {
withAnimation {
theCard.toggle()
cardOnTop.toggle() // comment me to test flipping alone
}
}
Color.white
} else {
Color.white
CardView(card: theCard)
.matchedGeometryEffect(id: 1, in: animationNameSpace)
.onTapGesture {
withAnimation {
theCard.toggle()
cardOnTop.toggle()
}
}
}
}
.padding()
}
struct Card {
var isFaceUp: Bool
mutating func toggle() {
isFaceUp.toggle()
}
}
struct CardView: View {
var card: Card
var body: some View {
Rectangle()
.frame(width: 100, height: 50)
.foregroundColor(.red)
.cardify(isFaceUp: card.isFaceUp)
}
}
}
/* Cardify ViewModifier */
struct Cardify: ViewModifier, Animatable {
init(isFaceUp: Bool){
rotation = isFaceUp ? 0 : 180
}
var rotation: Double // in degrees
var animatableData: Double {
get { return rotation }
set { rotation = newValue }
}
func body(content: Content) -> some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
if rotation < 90 {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: DrawingConstants.lineWidth).foregroundColor(.gray)
} else {
shape.fill().foregroundColor(.gray)
}
content
.opacity(rotation < 90 ? 1 : 0)
}
.rotation3DEffect(Angle.degrees(rotation), axis: (0, 1, 0))
}
private struct DrawingConstants {
static let cornerRadius: CGFloat = 15
static let lineWidth: CGFloat = 2
}
}
extension View {
func cardify(isFaceUp: Bool) -> some View {
return self.modifier(Cardify(isFaceUp: isFaceUp))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
How can I make the card flying with rotation?
Also, I found that this works (if put in the body of VStack alone, no if-branches)
CardView(card: theCard)
.offset(x: 0, y: cardOnTop ? 0 : 100)
.onTapGesture {
withAnimation {
theCard.toggle()
cardOnTop.toggle() // comment me to test flipping alone
}
}
but in my app I have a deck of flipped down cards, which are dealt to board flipping up. I tried to model the behavior by if-branch in the code of ContentView above (CardViews appear and dissapear).
Here is a solution purely with .transition using a custom transition:
struct ContentView: View {
#State var cardOnTop = true
var body: some View {
VStack(spacing:0) {
if cardOnTop {
RoundedRectangle(cornerRadius: 15)
.fill(.blue)
.transition(.rotate3D(direction: 1).combined(with: .move(edge: .bottom)))
Color.clear
} else {
Color.clear
RoundedRectangle(cornerRadius: 15)
.fill(.green)
.transition(.rotate3D(direction: -1).combined(with: .move(edge: .top)))
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 2)) {
cardOnTop.toggle()
}
}
.padding()
}
}
/* Transition Modifier */
extension AnyTransition {
static func rotate3D(direction: Double) -> AnyTransition {
AnyTransition.modifier(
active: Rotate3DModifier(value: 1, direction: direction),
identity: Rotate3DModifier(value: 0, direction: direction))
}
}
struct Rotate3DModifier: ViewModifier {
let value: Double
let direction: Double
func body(content: Content) -> some View {
content
.rotation3DEffect(Angle(degrees: 180 * value * direction), axis: (x: 0, y: 1, z: 0))
.opacity(1 - value)
}
}
The rotation isn't triggered because you remove the card and insert another one into the view, so only the .matchedGeometryEffect remains.
What you want is one card that stays in view so it can rotate, but also change its position.
You can achieve this e.g. with .offset. The GeometryReader is just to find the right offset value.
var body: some View {
GeometryReader { geo in
VStack {
CardView(card: theCard)
.offset(x: 0, y: cardOnTop ? 0 : geo.size.height / 2)
.onTapGesture {
withAnimation {
cardOnTop.toggle()
theCard.toggle()
}
}
Color.clear
}
}
.padding()
}

SwiftUI - Strange Animation

I have an image for a button. Intended result is so that when I click on the button/image, a list of items appear below it. However, for some reason the image itself shrinks upon clicking. How would I prevent the image from shrinking and just having the items populate below the image?
struct HabitGroups: View {
var imageName: String
var textOverlay: String
var listOfHabits: [String]
#Binding var selectedHabitList: [String]
#Binding var enoughHabits: Bool
#State var showHabits = false
var body: some View {
VStack {
Button(action: {
withAnimation {
self.showHabits.toggle()
}
}) {
Image(self.imageName)
.resizable()
.scaledToFit()
.frame(minWidth: 300, minHeight: 100)
.overlay(ImageOverlay(textOverlay: self.textOverlay), alignment: .bottom)
}
.buttonStyle(PlainButtonStyle())
if self.showHabits {
ForEach(self.listOfHabits, id: \.self) { habit in
AddHabitButtons(habit: habit, selectedHabitList: self.$selectedHabitList, enoughHabits: self.$enoughHabits)
}
}
}
}
}
Before Animation
After Animation
You have to many items below image to fit without resizing everything on screen (as you placed everything in on VStack), so image shrinks as it is .resizalbe.
Possible solution would be to embed habits into scroll view, like below
if self.showHabits {
ScrollView { // << here !!
ForEach(self.listOfHabits, id: \.self) { habit in
AddHabitButtons(habit: habit, selectedHabitList: self.$selectedHabitList, enoughHabits: self.$enoughHabits)
}
}
}
another alternate is to make image fixed by height, but in this case shown habits will push it up off screen. However intended behavior is not clear, anyway here it is
.buttonStyle(PlainButtonStyle())
.fixedSize(horizontal: false, vertical: true) // << here !!

SwiftUI: How to align view to HStack's subview?

I want to align the circle to the TileView "1"'s center at top left. Is there any other like centre constraint of UIView?
struct BoardView: View {
var body: some View {
ZStack {
VStack {
HStack {
TileView(number: 1)
TileView(number: 2)
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
}
}
}
Here is possible approach using custom alignment guides
extension VerticalAlignment {
private enum VCenterAlignment: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
return dimensions[VerticalAlignment.center]
}
}
static let vCenterred = VerticalAlignment(VCenterAlignment.self)
}
extension HorizontalAlignment {
private enum HCenterAlignment: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
return dimensions[HorizontalAlignment.center]
}
}
static let hCenterred = HorizontalAlignment(HCenterAlignment.self)
}
struct BoardView: View {
var body: some View {
ZStack(alignment: Alignment(horizontal: .hCenterred, vertical: .vCenterred)) {
VStack {
HStack {
TileView(number: 1)
.alignmentGuide(.vCenterred) { $0[VerticalAlignment.center] }
.alignmentGuide(.hCenterred) { $0[HorizontalAlignment.center] }
TileView(number: 2)
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
.alignmentGuide(.vCenterred) { $0[VerticalAlignment.center] }
.alignmentGuide(.hCenterred) { $0[HorizontalAlignment.center] }
}
}
}
Test module on GitHub
Using GeometryReader and preference
GeometryReader - let us to get geometryProxy of a view ( according to the parent view / space provide by parent )
Note:- child view stays at their own (parent cant force them position or size)
.preference(key:,value:) - this modifier let us get some values( geometryProxy ) out of the view. so that we can access child's geometry proxy from parent as well.
comment below for any question about these topics.
here is the code,
(plus point - you can animate your circle's position easily.(by adding .animation() modifier. Ex- if you want your circle move when user touch on a title)
complete code
import SwiftUI
struct ContentView: View {
#State var centerCoordinateOfRectangele: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
var body: some View {
ZStack {
VStack {
HStack {
GeometryReader { (geometry) in
TileView(number: 1)
.preference(key: MyPreferenceKey.self, value: MyPreferenceData(x: geometry.frame(in: .named("spaceIWantToMoveMyView")).midX, y: geometry.frame(in: .named("spaceIWantToMoveMyView")).midY))
}
TileView(number: 2)
}.onPreferenceChange(MyPreferenceKey.self) { (value) in
self.centerCoordinateOfRectangele = value
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
.position(x: centerCoordinateOfRectangele.x, y: centerCoordinateOfRectangele.y)
}
.coordinateSpace(name: "spaceIWantToMoveMyView") //to get values relative to this layout
// watch where i have use this name
}
}
struct TileView: View{
let number: Int
var body: some View{
GeometryReader { (geometry) in
RoundedRectangle(cornerRadius: 30)
}
}
}
struct MyPreferenceData:Equatable {
let x: CGFloat
let y: CGFloat
}
struct MyPreferenceKey: PreferenceKey {
typealias Value = MyPreferenceData
static var defaultValue: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
static func reduce(value: inout MyPreferenceData, nextValue: () -> MyPreferenceData) {
value = nextValue()
}
}
Code explanation ->
data model which I want to grab from the child view
struct MyPreferenceData:Equatable {
let x: CGFloat
let y: CGFloat
}
Key
struct MyPreferenceKey: PreferenceKey {
typealias Value = MyPreferenceData
static var defaultValue: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
static func reduce(value: inout MyPreferenceData, nextValue: () -> MyPreferenceData) {
value = nextValue()
}
}
defaultValue - SwiftUI use this, when there are no explicit value set
reduce() - getCalled when SwiftUI want to give values (we can use .preference() modifier on many views with the same key)
we add the .preference() modifier which we want to get values

SwiftUI Animate Image on Button

Using SwiftUI, I'm trying to show an animated image and hide the text when the user clicks the button. Here is my code:
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
HStack {
if self.showingActivity {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showingActivity ? 360.0 : 0.0))
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false))
}
else {
Text("Continue")
}
}
}
}
The Continue text appears and when clicked it disappears (as expected) and the image shows, but no animation. Any ideas what I might be doing wrong.
Thanks
or try this:
struct ImageView : View {
#State private var showAction = false
var body: some View {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showAction ? 360.0 : 0.0))
.animation(self.showAction ? Animation.linear(duration: 1.5).repeatForever(autoreverses: false) : nil)
.onAppear() {
self.showAction = true
}
}
}
struct ContentView: View {
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
HStack {
if self.showingActivity {
ImageView()
}
else {
Text("Continue")
}
}
}
}
}
You can use .opacity instead of if...else statements, in this case rotation works on image. But in canvas I still see some glitches, if tap button several times but I didn't try on real device (behavior there can be other). Hope it'll help:
struct AnimatableView: View {
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
ZStack {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showingActivity ? 360.0 : 0.0))
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false))
.opacity(self.showingActivity ? 1 : 0)
Text("Continue")
.opacity(self.showingActivity ? 0 : 1)
}
}
}
}

Animation in ScrollView is not working ? Using Xcode 11 beta 6

I was trying to to do transition animation Scrollview but found out that how things work differently in scrollView. But still unable to do animation in it. I am providing code, please have a look. Using Xcode 11 beta 6
import SwiftUI
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
NavigationView {
ScrollView{
VStack {
Button(action: {
// withAnimation {
self.isButtonVisible.toggle()
// }
}) {
Text("Press me")
}
if isButtonVisible {
Text("sss")
.frame(height: true ? 50 : 0, alignment: .center)
.background(Color.red)
.animation(.linear(duration: 2))
// .transition(.move(edge: .top))
}
}
}
}
}}
This must be a bug, and I suggest you file a bug report with Apple. I find a workaround (see code below), but it unfortunately uncovers another bug!
In order to make the animation inside the ScrollView to work, you can encapsulate the contents in a custom view. That will fix that problem.
This will uncover a new issue, which is made evident by the borders I added to your code: when the Text view is added, it shifts parts of the contents outside the ScrollView.
You will see that this is not correct. Try starting your app with a default value isButtonVisible = true, and you will see it renders it differently.
struct ContentView : View {
var body: some View {
NavigationView {
ScrollView {
EncapsulatedView().border(Color.green)
}.border(Color.blue)
}
}
}
struct EncapsulatedView: View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Text("Filler")
Button(action: {
withAnimation(.easeInOut(duration: 2.0)) {
self.isButtonVisible.toggle()
}
}) {
Text("Press me")
}
if isButtonVisible {
Text("sss")
.frame(height: true ? 50 : 0, alignment: .center)
.transition(.opacity)
.background(Color.red)
}
Spacer()
}.border(Color.red)
}
}

Resources