How to do a "reveal"-style collapse/expand animation in SwiftUI? - swift

I'd like to implement an animation in SwiftUI that "reveals" the content of a view to enable expand/collapse functionality. The content of the view I want to collapse and expand is complex: It's not just a simple box, but it's a view hierarchy of dynamic height and content, including images and text.
I've experimented with different options, but it hasn't resulted in the desired effect. Usually what happens is that when I "expand", the whole view was shown right away with 0% opacity, then gradually faded in, with the buttons under the expanded view moving down at the same time. That's what happened when I was using a conditional if statement that actually added and removed the view. So that makes sense.
I then experimented with using a frame modifier: .frame(maxHeight: isExpanded ? .infinity : 0). But that resulted in the contents of the view being "squished" instead of revealed.
I made a paper prototype of what I want:
Any ideas on how to achieve this?

Something like this might work. You can modify the height of what you want to disclose to be 0 when hidden or nil when not so that it'll go for the height defined by the views. Make sure to clip the view afterwards so the contents are not visible outside of the frame's height when not disclosed.
struct ContentView: View {
#State private var isDisclosed = false
var body: some View {
VStack {
Button("Expand") {
withAnimation {
isDisclosed.toggle()
}
}
.buttonStyle(.plain)
VStack {
GroupBox {
Text("Hi")
}
GroupBox {
Text("More details here")
}
}
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
HStack {
Text("Cancel")
Spacer()
Text("Book")
}
}
.frame(maxWidth: .infinity)
.background(.thinMaterial)
.padding()
}
}
No, this wasn't trying to match your design, either. This was just to provide a sample way of creating the animation.

Consider the utilization of DisclosureGroup. The following code should be a good approach to your idea.
struct ContentView: View {
var body: some View {
List(0...20, id: \.self) { idx in
DisclosureGroup {
HStack {
Image(systemName: "person.circle.fill")
VStack(alignment: .leading) {
Text("ABC")
Text("Test Test")
}
}
HStack {
Image(systemName: "globe")
VStack(alignment: .leading) {
Text("ABC")
Text("X Y Z")
}
}
HStack {
Image(systemName: "water.waves")
VStack(alignment: .leading) {
Text("Bla Bla")
Text("123")
}
}
HStack{
Button("Cancel", role: .destructive) {}
Spacer()
Button("Book") {}
}
} label: {
HStack {
Spacer()
Text("Expand")
}
}
}
}
The result looks like:
I coded this in under 5 minutes. So of course the design can be optimized to your demands, but the core should be understandable.

import SwiftUI
struct TaskViewCollapsible: View {
#State private var isDisclosed = false
let header: String = "Review Page"
let url: String
let tasks: [String]
var body: some View {
VStack {
HStack {
VStack(spacing: 5) {
Text(header)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.black)
.padding(.top, 10)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
Text(url)
.font(.system(size: 12, weight: .regular))
.foregroundColor(.black.opacity(0.4))
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
Image(systemName: self.isDisclosed ? "chevron.up" : "chevron.down")
.padding(.trailing)
.padding(.top, 10)
}
.onTapGesture {
withAnimation {
isDisclosed.toggle()
}
}
FetchTasks()
.padding(.horizontal, 20)
.padding(.bottom, 5)
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
}
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.black.opacity(0.2))
)
.frame(maxWidth: .infinity)
.padding()
}
#ViewBuilder
func FetchTasks() -> some View {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0 ..< tasks.count, id: \.self) { value in
Text(tasks[value])
.font(.system(size: 16, weight: .regular))
.foregroundColor(.black)
.padding(.vertical, 0)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.frame(maxHeight: CGFloat(tasks.count) * 20)
}
}
struct TaskViewCollapsible_Previews: PreviewProvider {
static var previews: some View {
TaskViewCollapsible(url: "trello.com", tasks: ["Hello", "Hello", "Hello"])
}
}

Related

Ways to display data in SwiftUI - Alternatives to List

I'm display how many steps I have taken but don't want it in a list. What should I use instead? Thanks
NavigationView {
List(steps, id: \.id) { step in
VStack(spacing: 15){
Text("\(step.count)")
.font(.custom(customFont, size: 100))
.fontWeight(.semibold)
.multilineTextAlignment(.center)
.opacity(5)
.aspectRatio(contentMode: .fill)
.frame(width: 200, height: 200)
.padding(.bottom, -45)
HStack{
Text("Steps Today")
.font(.custom(customFont, size: 40))
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, alignment: .center)
}
following #aheze and generally speaking it should look something like this:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
LazyVStack {
ForEach(steps, id: \.id) { step in
NavigationLink {
// here comes the destination view, this is only dummy
Text("I walked \(step.count) steps")
} label: {
Text("\(step.count)")
.font(.system(size: 100))
.fontWeight(.semibold)
}
}
}
}
.navigationBarHidden(true)
}
}
}

Adding a "Hamburger" Menu to IOS application

I am trying to create a hamburger menu that when you click the "hamburger" (three horizontal lines) button, the menu will slide out. I am following the tutorial found here, but the only thing that isn't working is the lines for the Hamburger image is not showing up on my application. Everything else works, but for some reason this is the one thing that is not working.
Here is my code for the ContentView, where it hosts the problem code
struct ContentView: View {
#State var showMenu = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
MainView(showMenu: self.$showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/2 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView()
.frame(width: geometry.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
.navigationBarTitle("Side Menu", displayMode: .inline) // this works
//somewhere below here is the problem
.navigationBarItems(leading: (
Button(action: {
withAnimation {
self.showMenu.toggle()
}
}) {
Image(systemName: "three_horizontal_lines")
.imageScale(.large)
}
))
}
}
}
Here is the MainView:
struct MainView: View{
#Binding var showMenu: Bool
var body: some View{
Button(action: {
withAnimation{
self.showMenu = true
}
}){
Text("Show Menu")
}
}
}
Lastly, this is the MenuView:
struct MenuView: View{
var body: some View{
VStack(alignment: .leading){
HStack{
Image(systemName: "person")
.foregroundColor(.gray)
.imageScale(.large)
Text("Profile")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 100)
HStack{
Image(systemName: "envelope")
.foregroundColor(.gray)
.imageScale(.large)
Text("Messages")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 30)
HStack{
Image(systemName: "gear")
.foregroundColor(.gray)
.imageScale(.large)
Text("Settings")
.foregroundColor(.gray)
.font(.headline)
}
.padding(.top, 30)
Spacer()
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
}
}
I have checked for any misspellings and even copied and pasted the code from the original tutorial, but it seems nothing allows me to see the burger image like is shown on the tutorial. Any thoughts on what else I could try?

Boundary on top of NavigationView

When I was trying to change the color of my navigation view page I realized that there is a weird boundary on top. I can't figure out what it is or how to get rid of it. Would anyone happen to know?
Here is the code.
Image with displayMode: .inline
The parent view code is the view that is presenting the page I am having trouble with.
Parent View:
Code:
import Foundation
import SwiftUI
import UIKit
struct ContentView: View {
// variable for view model
#ObservedObject var viewModel = VariableViewModel()
// SWIFT UI START
var body: some View {
// Main page
NavigationView {
ZStack {
Color(.orange).edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
VStack {
HStack {
Spacer()
NavigationLink(destination:
SettingsView()
){
Image(systemName: "gearshape.fill").font(.system(size: 25))
}
Spacer()
Spacer()
Spacer()
Spacer()
Spacer()
Spacer()
NavigationLink(destination:
Text("You")
){
Image(systemName: "chart.bar.xaxis").font(.system(size: 25))
}
Spacer()
}
Text("Pick a mode!").font(.largeTitle).bold().offset(x: 0, y: 30)
ZStack {
VStack {
Spacer()
// ADDITION SECTION
NavigationLink(destination:
VStack {
Spacer()
MathView(operatorName: "Addition")
}
){
HStack {
Text("Addition")
Image(systemName: "plus.square")
}.font(.largeTitle)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.padding(10)
.border(Color.blue, width: 5)
}
Spacer()
// SUBTRACTION SECTION
NavigationLink(destination:
VStack {
Spacer()
MathView(operatorName: "Subtraction")
}
){
HStack {
Text("Subtraction")
Image(systemName: "minus.square")
}.font(.largeTitle)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.padding(10)
.border(Color.blue, width: 5)
}
Spacer()
// MULTIPLICATION SECTION
NavigationLink(destination:
VStack {
Spacer()
MathView(operatorName: "Multiplication")
}
){
HStack {
Text("Multiplication")
Image(systemName: "multiply.square")
}.font(.largeTitle)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.padding(10)
.border(Color.blue, width: 5)
}
Spacer()
// DIVISION SECTION
NavigationLink(destination:
VStack {
Spacer()
MathView(operatorName: "Division")
}
){
HStack {
Text("Division")
Image(systemName: "divide.square")
}.font(.largeTitle)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.padding(10)
.border(Color.blue, width: 5)
}
}
}.navigationBarHidden(true)
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Code:
import SwiftUI
struct MathView: View {
#ObservedObject var viewModel = VariableViewModel()
let operatorName: String
var body: some View {
ZStack {
Color.orange.edgesIgnoringSafeArea(.all)
VStack {
Spacer()
NavigationLink(destination:
MathContentView(operatorName: "Addition", operatorSymbol: "plus", difficultyNumber1: 5, difficultyNumber2: 5)
) {
Text("Easy")
.font(.title2)
.padding(35)
.foregroundColor(.white)
.background(Color(.systemGreen))
.cornerRadius(40)
.onAppear(perform: {
if operatorName == "Addition" {
self.viewModel.result = self.viewModel.num1 + self.viewModel.num2
} else if operatorName == "Subtraction" {
self.viewModel.result = self.viewModel.num1 - self.viewModel.num2
} else if operatorName == "Multiplication" {
self.viewModel.result = self.viewModel.num1 * self.viewModel.num2
};
withAnimation {
viewModel.resetVariables()
// numbers generator
}
})
}
Spacer()
}
}
}
}
To remove the empty space below the NavigationView add .navigationBarTitleDisplayMode(.inline) to the top view:
ZStack {
// ...
}
.navigationBarTitleDisplayMode(.inline)
Then, the slim line between the navigationViewTitle and the content below comes from the Spacer at the top of the VStack in NavigationLink that pushes the MathView.
NavigationLink(destination:
VStack {
Spacer() // this causes the *slim line*
MathView(operatorName: "Addition")
}
)
You need to remove the Spacer (and the VStack as well):
NavigationLink(destination:
MathView(operatorName: "Addition")
)

Make SwiftUI Rectangle same height or width as another Rectangle

For a SwiftUI layout in a macOS app, I have three Rectangles as shown below:
The code to produce this layout is:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
HStack {
ZStack {
Rectangle()
.fill(Color.purple)
.frame(width: 20)
Text("1")
.font(.subheadline)
.foregroundColor(.white)
}
ZStack {
Rectangle()
.fill(Color.orange)
Text("2")
.font(.subheadline)
.foregroundColor(.white)
}
}
HStack {
ZStack {
Rectangle()
.fill(Color.red)
.frame(height: 20)
Text("3")
.font(.subheadline)
.foregroundColor(.white)
}
}
}
.frame(minWidth: 400, minHeight: 250)
}
}
My objective is for Rectangle 1 to be the same height as Rectangle 2 and for Rectangle 3 to be the same width as Rectangle 2. The size relationships between the rectangles should stay the same as the window size is changed. When done correctly, the final result should look like the following:
How can I accomplish this in SwiftUI?
Here is a working approach, based on view preferences. Tested with Xcode 11.4 / macOS 10.15.6
struct ViewWidthKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
struct ContentView: View {
#State private var boxWidth = CGFloat.zero
var body: some View {
VStack {
HStack {
ZStack {
Rectangle()
.fill(Color.purple)
.frame(width: 20)
Text("1")
.font(.subheadline)
.foregroundColor(.white)
}
ZStack {
Rectangle()
.fill(Color.orange)
Text("2")
.font(.subheadline)
.foregroundColor(.white)
}
.background(GeometryReader {
Color.clear.preference(key: ViewWidthKey.self,
value: $0.frame(in: .local).size.width) })
}
HStack {
ZStack {
Rectangle()
.fill(Color.red)
.frame(height: 20)
Text("3")
.font(.subheadline)
.foregroundColor(.white)
}.frame(width: boxWidth)
}.frame(maxWidth: .infinity, alignment: .bottomTrailing)
}
.onPreferenceChange(ViewWidthKey.self) { self.boxWidth = $0 }
.frame(minWidth: 400, minHeight: 250)
}
}

SwiftUI HStack with equal Height

I want the Text("111") to have the equal height of the VStack containing 2222... and 333....
struct Test7: View {
var body: some View
{ HStack (alignment: .top) {
Text( "111") // Shall have equal Height
.background(Color.red)
VStack(alignment: .leading){. // of this VStack
Text("2222222")
.background(Color.gray)
Text("333333333")
.background(Color.blue)
}
}
.background(Color.yellow)}
}
I tried with a GeometryReaderbut didn't get it to work
Here is possible approach using .alignmentGuide
struct Test7: View {
#State
private var height: CGFloat = .zero // < calculable height
var body: some View {
HStack (alignment: .top) {
Text( "111")
.frame(minHeight: height) // in Preview default is visible
.background(Color.red)
VStack(alignment: .leading) {
Text("2222222")
.background(Color.gray)
Text("333333333")
.background(Color.blue)
}
.alignmentGuide(.top, computeValue: { d in
DispatchQueue.main.async { // << dynamically detected - needs to be async !!
self.height = max(d.height, self.height)
}
return d[.top]
})
}
.background(Color.yellow)
}
}
Note: real result is visible only in LivePreview, because height is calculated dynamically and assign in next rendering cycle to avoid conflicts on #State.
use .frame(maxHeight: .infinity)
var body: some View {
HStack(alignment: .top) {
Text("111")
.frame(maxHeight: .infinity)
.background(Color.red)
VStack(alignment: .leading) {
Text("2222222")
.frame(maxHeight: .infinity)
.background(Color.gray)
Text("333333333")
.frame(maxHeight: .infinity)
.background(Color.blue)
}
}.background(Color.yellow)
.frame(height: 50)
}
result: demo