struct ReversedScrollView: View {
var body: some View {
}
}
struct ReversedScrollView: View {
var body: some View {
GeometryReader { proxy in
ScrollView(.horizontal) {
}
.background(Color.gray.opacity(0.3))
}
}
}

GeometryReader
so that our scrollView can take the available size. Then we are declaring our ScrollView
with horizontal scroll behaviour. Now we will need some content to show in scrollView.struct ReversedScrollView<Content: View>: View {
var content: Content
init(@ViewBuilder builder: ()->Content) {
self.content = builder()
}
var body: some View {
GeometryReader { proxy in
ScrollView(.horizontal) {
content
}
.background(Color.gray.opacity(0.3))
}
}
}
struct ReversedScrollView_Previews: PreviewProvider {
static var previews: some View {
ReversedScrollView {
Text("Hey scrollview")
}
}
}

struct ReversedScrollView<Content: View>: View {
var content: Content
init(@ViewBuilder builder: ()->Content) {
self.content = builder()
}
var body: some View {
GeometryReader { proxy in
ScrollView(.horizontal) {
HStack {
Spacer()
content
}
.background(Color.gray.opacity(0.2))
}
.background(Color.gray.opacity(0.3))
}
}
}

content
to HStack
with a Spacer()
so that we can have spacing on the left side of the content.If we now build and run then it will not do as we expected. This is because HStack will layout it according to its children. struct ReversedScrollView_Previews: PreviewProvider {
static var previews: some View {
ReversedScrollView {
ForEach(0..<3) { _ in
Text("Hey scrollview")
}
}
}
}

ReversedScrollView {
ForEach(0..<5) { _ in
Text("Hey scrollview")
}
}

struct ReversedScrollView<Content: View>: View {
var content: Content
init(@ViewBuilder builder: ()->Content) {
self.content = builder()
}
var body: some View {
GeometryReader { proxy in
ScrollView(.horizontal) {
HStack {
Spacer()
content
}
.frame(minWidth: proxy.size.width)
.background(Color.gray.opacity(0.2))
}
.background(Color.gray.opacity(0.3))
}
}
}
struct ReversedScrollView_Previews: PreviewProvider {
static var previews: some View {
ReversedScrollView {
ForEach(0..<5) { item in
Text("\(item)")
.padding()
.background(Color.gray.opacity(0.5))
.cornerRadius(6)
}
}
}
}

HStack
for the minimum width of the GeometryProxy's width
, our HStack
will now fit the whole available width. That will allow the spacer to send the content to the opposite edge.Since we are setting the minimumWidth
, it will expand the width as the content grows.That's it. We have it. A scrollView
with reversed scrolling.
Changing axis
Now we have implemented for horizontal axis. Lets go to support the both axis.struct ReversedScrollView<Content: View>: View {
var axis: Axis.Set
var content: Content
init(_ axis: Axis.Set = .horizontal, @ViewBuilder builder: ()->Content) {
self.axis = axis
self.content = builder()
}
var body: some View {
GeometryReader { proxy in
ScrollView(axis) {
HStack {
Spacer()
content
}
.frame(minWidth: proxy.size.width)
}
}
}
}
axis
variable and initialised with the default `.horizontal` axis and updated our body property according to it. Now we need to update the HStack
to respect the requested axis.So we can create new container view `Stack` which will take axis argument and return the appropriate Stack.struct Stack<Content: View>: View {
var axis: Axis.Set
var content: Content
init(_ axis: Axis.Set = .vertical, @ViewBuilder builder: ()->Content) {
self.axis = axis
self.content = builder()
}
var body: some View {
switch axis {
case .horizontal:
HStack {
content
}
case .vertical:
VStack {
content
}
default:
VStack {
content
}
}
}
}
Read also : How to get started with RxSwiftNow updating our scrollview implementation.
struct ReversedScrollView<Content: View>: View {
var axis: Axis.Set
var content: Content
init(_ axis: Axis.Set = .horizontal, @ViewBuilder builder: ()->Content) {
self.axis = axis
self.content = builder()
}
var body: some View {
GeometryReader { proxy in
ScrollView(axis) {
Stack(axis) {
Spacer()
content
}
.frame(minWidth: proxy.size.width)
}
}
}
}

frame(minWidth:)
modifier we set for horizontal case. So we need to update that also. 
minWidth
in case of horizontal axis and minHeight
in case of vertical axis. so in either of other value can be nil
in the frame modifier. So we add two functions like below.func minWidth(in proxy: GeometryProxy, for axis: Axis.Set) -> CGFloat? {
axis.contains(.horizontal) ? proxy.size.width : nil
}
func minHeight(in proxy: GeometryProxy, for axis: Axis.Set) -> CGFloat? {
axis.contains(.vertical) ? proxy.size.height : nil
}
var body: some View {
GeometryReader { proxy in
ScrollView(axis) {
Stack(axis) {
Spacer()
content
}
.frame(
minWidth: minWidth(in: proxy, for: axis),
minHeight: minHeight(in: proxy, for: axis)
)
}
}
struct ReversedScrollView_Previews: PreviewProvider {
static var previews: some View {
ReversedScrollView(.vertical) {
ForEach(0..<5) { item in
Text("\(item)")
.padding()
.background(Color.gray.opacity(0.5))
.cornerRadius(6)
}
.frame(maxWidth: .infinity)
}
}
}

The Leading space
Here we can see the first item starts at the very leading, we want to have some spacing there, so we can have some additional views like a add button.We can do that by adding the `minLengh` to the our spacer like below.var body: some View {
GeometryReader { proxy in
ScrollView(axis) {
Stack(axis) {
Spacer(minLength: leadingSpace)
content
}
.frame(
minWidth: minWidth(in: proxy, for: axis),
minHeight: minHeight(in: proxy, for: axis)
)
}
}
}
struct ReversedScrollView<Content: View>: View {
var axis: Axis.Set
var leadingSpace: CGFloat
var content: Content
init(_ axis: Axis.Set = .horizontal, leadingSpace: CGFloat = 10, @ViewBuilder builder: ()->Content) {
self.axis = axis
self.leadingSpace = leadingSpace
self.content = builder()
}
struct ReversedScrollView_Previews: PreviewProvider {
static var previews: some View {
ReversedScrollView(.horizontal, leadingSpace: 50) {
ForEach(0..<12) { item in
Text("\(item)")
.padding()
.background(Color.gray.opacity(0.5))
.cornerRadius(6)
}
}
}
}
Use cases
1. Emoji Collectorstruct EmojiCollector: View {
@State private var emojies: [String] = []
var body: some View {
ZStack {
ReversedScrollView(.horizontal, leadingSpace: 100) {
ForEach(emojies.reversed(), id: \.self) { emoji in
Text(emoji)
.font(.largeTitle)
.padding()
.background(Color.green.opacity(0.5))
.cornerRadius(8)
.transition(.move(edge: .bottom))
.frame(maxHeight: .infinity)
}
}
HStack {
Button(action: addEmoji) {
ZStack {
Circle()
.fill(Color.green)
.shadow(color: Color.green, radius: 4, x: 0, y: 0)
Image(systemName: "plus")
.font(.largeTitle)
.foregroundColor(.white)
}
}
.buttonStyle(PlainButtonStyle())
.frame(width: 60, height: 60)
Spacer()
}
.padding()
}
.frame(height: 150)
}
func addEmoji() {
withAnimation(.default) {
if let random = String.emojies.randomElement() {
self.emojies.append(random)
}
}
}
}
extension String {
static var emojies: [String] {
"๐,๐,๐,๐,๐,๐
,๐,๐คฃ,โบ๏ธ,๐,๐,๐,๐,๐,๐,๐,๐ฅฐ,๐,๐,๐,๐,๐,๐,๐,๐,๐คช,๐คจ,๐ง,๐ค,๐,๐คฉ,๐ฅณ,๐,๐,๐,๐,๐,๐,๐,โน๏ธ,๐ฃ,๐,๐ซ,๐ฉ,๐ฅบ,๐ข,๐ญ,๐ค,๐ ,๐ก,๐คฌ,๐คฏ,๐ณ,๐ฅต,๐ฅถ,๐ฑ,๐จ,๐ฐ,๐ฅ,๐,๐ค,๐ค,๐คญ,๐คซ,๐คฅ,๐ถ,๐,๐,๐ฌ,๐,๐ฏ,๐ฆ,๐ง,๐ฎ,๐ฒ,๐ฅฑ,๐ด,๐คค,๐ช,๐ต,๐ค,๐ฅด,๐คข,๐คฎ,๐คง,๐ท,๐ค,๐ค,๐ค"
.split(separator: ",")
.compactMap({"\($0)"})
}
}
2. Chat Screenstruct ChatView: View {
@State private var messages: [Message] = []
@State private var text: String = ""
@State private var targetMessage: Message?
var body: some View {
NavigationView {
VStack(spacing: 0) {
ScrollViewReader { scrollView in
ReversedScrollView(.vertical, showsIndicators: false) {
ForEach(messages) { message in
MessageView(message: message)
.transition(.move(edge: .bottom))
}
}
.padding(.horizontal)
.onChange(of: targetMessage) { message in
if let message = message {
targetMessage = nil
withAnimation(.default) {
scrollView.scrollTo(message.id)
}
}
}
HStack {
TextField("Message", text: $text)
.frame(height: 44)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
send()
}) {
Text("Send")
}
.buttonStyle(AppButtonStyle())
}
.padding(.horizontal)
}
}
.navigationBarTitle("Chat")
}
}
func send() {
guard text.hasText else { return }
let message = Message(id: UUID(), text: self.text, userId: "1", type: .random)
self.messages.append(message)
self.text = ""
self.targetMessage = message
}
}
struct ChatView_Previews: PreviewProvider {
static var previews: some View {
ChatView()
}
}
extension Date {
static var formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm"
return formatter
}()
}
// MARK: - Data
enum RecieptType: Int, Codable, Equatable {
case sent
case received
}
struct Message: Codable, Hashable, Identifiable {
var id: UUID
var text: String
var userId: String
var type: RecieptType
var date: Date = Date()
}
extension RecieptType {
var backgroundColor: Color {
switch self {
case .sent:
return .green
case .received:
return Color(#colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1))
}
}
var textColor: Color {
switch self {
case .sent:
return .white
case .received:
return .black
}
}
static var random: RecieptType {
let random = Int.random(in: 0...1)
return RecieptType(rawValue: random) ?? .sent
}
}
// MARK: - MessageView
struct MessageView: View {
var message: Message
var body: some View {
HStack {
if message.type == .sent {
Spacer(minLength: 16)
}
VStack(alignment: .trailing, spacing: 0) {
Text(message.text)
.padding(8)
Text("\(message.date, formatter: Date.formatter)")
.font(.system(size: 13))
.padding(6)
}
.background(message.type.backgroundColor)
.foregroundColor(message.type.textColor)
.cornerRadius(8)
if message.type == .received {
Spacer(minLength: 16)
}
}
.frame(maxWidth: .infinity)
.id(message.id)
}
}
Thanks for the reading.

