SwiftUI - stacking views like the notifications on the Lockscreen - ios

I am trying to stack views of my app like the notifications on the lockscreen. (See attachment) I want them to be able to stack by clicking the button and expand on a click on the view. I have tried many things on VStack and setting the offsets of the other views but without success.
Is there maybe a standardized and easy way of doing this?
Somebody else have tried it and has a tip for me programming this?
Thanks
Notifications stacked
Notifications expanded
EDIT:
So this is some Code that I've tried:
VStack{
ToDoItemView(toDoItem: ToDoItemModel.toDo1)
.background(Color.green)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.zIndex(2.0)
ToDoItemView(toDoItem: ToDoItemModel.toDo2)
.background(Color.blue)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.zIndex(1.0)
.offset(x: 0.0, y: offset)
ToDoItemView(toDoItem: ToDoItemModel.toDo3)
.background(Color.yellow)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.offset(x: 0.0, y: offset1)
ToDoItemView(toDoItem: ToDoItemModel.toDo3)
.background(Color.gray)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.offset(x: 0.0, y: offset1)
ToDoItemView(toDoItem: ToDoItemModel.toDo3)
.background(Color.pink)
.cornerRadius(20.0)
.padding(.horizontal, 8)
.padding(.bottom, -1)
.offset(x: 0.0, y: offset1)
}
Button(action: {
if(!stacked){
stacked.toggle()
withAnimation(.easeOut(duration: 0.5)) { self.offset = -100.0 }
withAnimation(.easeOut(duration: 0.5)) { self.offset1 = -202.0 }
}
else {
stacked.toggle()
withAnimation(.easeOut(duration: 0.5)) { self.offset = 0 }
withAnimation(.easeOut(duration: 0.5)) { self.offset1 = 0 }
}
}, label: {
Text("Button")
})
So the first problem is that view 4 and 5 are not stacked on z behind view 3.
The second problem is that the button is not getting under the 5th view. (the frame/VStack of the views is still the old size)
Views stacked vertical
Views stacked on z

Another way using ZStack if your interested.
Note-:
I have declared variables globally in view, you can create your own model if you wish.
struct ContentViewsss: View {
#State var isExpanded = false
var color :[Color] = [Color.green,Color.gray,Color.blue,Color.red]
var opacitys :[Double] = [1.0,0.7,0.5,0.3]
var height:CGFloat
var width:CGFloat
var offsetUp:CGFloat
var offsetDown:CGFloat
init(height:CGFloat = 100.0,width:CGFloat = 400.0,offsetUp:CGFloat = 10.0,offsetDown:CGFloat = 10.0) {
self.height = height
self.width = width
self.offsetUp = offsetUp
self.offsetDown = offsetDown
}
var body: some View {
ZStack{
ForEach((0..<color.count).reversed(), id: \.self){ index in
Text("\(index)")
.frame(width: width , height:height)
.padding(.horizontal,isExpanded ? 0.0 : CGFloat(-index * 4))
.background(color[index])
.cornerRadius(height/2)
.opacity(isExpanded ? 1.0 : opacitys[index])
.offset(x: 0.0, y: isExpanded ? (height + (CGFloat(index) * (height + offsetDown))) : (offsetUp * CGFloat(index)))
}
Button(action:{
withAnimation {
if isExpanded{
isExpanded = false
}else{
isExpanded = true
}
}
} , label: {
Text(isExpanded ? "Stack":"Expand")
}).offset(x: 0.0, y: isExpanded ? (height + (CGFloat(color.count) * (height + offsetDown))) : offsetDown * CGFloat(color.count * 3))
}.padding()
Spacer().frame(height: 550)
}
}

I find it a little easier to get exact positioning using GeometryReader than using ZStack and trying to fiddle with offsets/positions.
This code has some numbers hard-coded in that you might want to make more dynamic, but it does function. It uses a lot of ideas you were already doing (zIndex, offset, etc):
struct ToDoItemModel {
var id = UUID()
var color : Color
}
struct ToDoItemView : View {
var body: some View {
RoundedRectangle(cornerRadius: 20).fill(Color.clear)
.frame(height: 100)
}
}
struct ContentView : View {
#State private var stacked = true
var todos : [ToDoItemModel] = [.init(color: .green),.init(color: .pink),.init(color: .orange),.init(color: .blue)]
func offsetForIndex(_ index : Int) -> CGFloat {
CGFloat((todos.count - index - 1) * (stacked ? 4 : 104) )
}
func horizontalPaddingForIndex(_ index : Int) -> CGFloat {
stacked ? CGFloat(todos.count - index) * 4 : 4
}
var body: some View {
VStack {
GeometryReader { reader in
ForEach(Array(todos.reversed().enumerated()), id: \.1.id) { (index, item) in
ToDoItemView()
.background(item.color)
.cornerRadius(20)
.padding(.horizontal, horizontalPaddingForIndex(index))
.offset(x: 0, y: offsetForIndex(index))
.zIndex(Double(index))
}
}
.frame(height:
stacked ? CGFloat(100 + todos.count * 4) : CGFloat(todos.count * 104)
)
.border(Color.red)
Button(action: {
withAnimation {
stacked.toggle()
}
}) {
Text("Unstack")
}
Spacer()
}
}
}

Related

Expandable Custom Segmented Picker in SwiftUI

I'm trying to create an expandable segmented picker in SwiftUI, I've done this so far :
struct CustomSegmentedPicker: View {
#Binding var preselectedIndex: Int
#State var isExpanded = false
var options: [String]
let color = Color.orange
var body: some View {
HStack {
ScrollView(.horizontal) {
HStack(spacing: 4) {
ForEach(options.indices, id:\.self) { index in
let isSelected = preselectedIndex == index
ZStack {
Rectangle()
.fill(isSelected ? color : .white)
.cornerRadius(30)
.padding(5)
.onTapGesture {
preselectedIndex = index
withAnimation(.easeInOut(duration: 0.5)) {
isExpanded.toggle()
}
}
}
.shadow(color: Color(UIColor.lightGray), radius: 2)
.overlay(
Text(options[index])
.fontWeight(isSelected ? .bold : .regular)
.foregroundColor(isSelected ? .white : .black)
)
.frame(width: 80)
}
}
}
.transition(.move(edge: .trailing))
.frame(width: isExpanded ? 80 : CGFloat(options.count) * 80 + 10, height: 50)
.background(Color(UIColor.cyan))
.cornerRadius(30)
.clipped()
Spacer()
}
}
}
Which gives this result :
Now, when it contracts, how can I keep showing the item selected and hide the others ? (for the moment, the item on the left is always shown when not expanded)
Nice job. You can add an .offset() to the contents of the ScollView, which shifts it left depending on the selection:
HStack {
ScrollView(.horizontal) {
HStack(spacing: 4) {
ForEach(options.indices, id:\.self) { index in
let isSelected = preselectedIndex == index
ZStack {
Rectangle()
.fill(isSelected ? color : .white)
.cornerRadius(30)
.padding(5)
.onTapGesture {
preselectedIndex = index
withAnimation(.easeInOut(duration: 0.5)) {
isExpanded.toggle()
}
}
}
.shadow(color: Color(UIColor.lightGray), radius: 2)
.overlay(
Text(options[index])
.fontWeight(isSelected ? .bold : .regular)
.foregroundColor(isSelected ? .white : .black)
)
.frame(width: 80)
}
}
.offset(x: isExpanded ? CGFloat(-84 * preselectedIndex) : 0) // <<< here
}
.transition(.move(edge: .trailing))
.frame(width: isExpanded ? 80 : CGFloat(options.count) * 80 + 10, height: 50)
.background(Color(UIColor.cyan))
.cornerRadius(30)
.clipped()
Spacer()
}
Here is another approach using .matchedGeometryEffect, which can handle different label widths without falling back to GeometryReader.
Based on expansionState it either draws only the selected item or all of them and .matchedGeometryEffect makes sure the animation goes smooth.
struct CustomSegmentedPicker: View {
#Binding var preselectedIndex: Int
#State var isExpanded = false
var options: [String]
let color = Color.orange
#Namespace var nspace
var body: some View {
HStack {
HStack(spacing: 8) {
if isExpanded == false { // show only selected option
optionLabel(index: preselectedIndex)
.id(preselectedIndex)
.matchedGeometryEffect(id: preselectedIndex, in: nspace, isSource: true)
} else { // show all options
ForEach(options.indices, id:\.self) { index in
optionLabel(index: index)
.id(index)
.matchedGeometryEffect(id: index, in: nspace, isSource: true)
}
}
}
.padding(5)
.background(Color(UIColor.cyan))
.cornerRadius(30)
Spacer()
}
}
func optionLabel(index: Int) -> some View {
let isSelected = preselectedIndex == index
return Text(options[index])
.fontWeight(isSelected ? .bold : .regular)
.foregroundColor(isSelected ? .white : .black)
.padding(8)
.background {
Rectangle()
.fill(isSelected ? color : .white)
.cornerRadius(30)
}
.onTapGesture {
preselectedIndex = index
withAnimation(.easeInOut(duration: 0.5)) {
isExpanded.toggle()
}
}
}
}

SwiftUI: Animate inserted values to line graph

I have created a line graph of a fixed width that takes in "data" (an array of doubles), using this tutorial. Here is my code:
struct ChartView: View {
var data: [Double]
private let maxY: Double = Double(Constants.maxRange)
private let minY: Double = 0
var body: some View {
VStack (spacing: 0) {
HStack (spacing: 0) {
chartYAxis
.frame(width: 40)
Divider()
chartView
.background(chartBackground)
Divider()
}
HStack (spacing: 0) {
Rectangle()
.fill(Color.clear)
.frame(width: 40, height: 20)
chartXAxis
}
}
.frame(height: 300)
.font(.caption)
.foregroundColor(Color("TextColor"))
}
}
extension ChartView {
private var chartView: some View {
GeometryReader { geometry in
Path { path in
for index in data.indices {
let xPosition = geometry.size.width / CGFloat(data.count - 1) * CGFloat(index)
let yAxis = maxY - minY
let yPosition = (1 - CGFloat((data[index] - minY) / yAxis)) * geometry.size.height
if (index == 0) {
path.move(to: CGPoint(x: xPosition, y: yPosition))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition ))
}
}
.stroke(Color.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
}
}
private var chartBackground: some View {
VStack {
Divider()
Spacer()
Divider()
Spacer()
Divider()
}
}
private var chartYAxis: some View {
VStack {
Text("\(maxY, specifier: "%.0f")")
Spacer()
let midY = (maxY + minY) / 2
Text("\(midY, specifier: "%.0f")")
Spacer()
Text("\(minY, specifier: "%.0f")")
}
}
private var chartXAxis: some View {
HStack {
ForEach(0..<data.count, id: \.self) { index in
Text("\(index + 1)")
if (index < (data.count - 1)) {
Spacer()
}
}
}
}
}
The array "data" will append values as per button press by the user, and at any time should have around 2-20 values.
Every time value is appended to "data", I want to animate this change. The animation should not redraw the line from left to right (like here) but squeeze the existing line together to make room for the appended value. Here is a very janky example of how I want the graph to work. However, I would like the changes to be smoother, similar to this example.
I want the graph frame to remain the same size with appended values to "data", not to scroll to add new values as in this question.
I have looked at some examples of animatable vectors in line graphs, but none address this problem I am having.
Does anyone know if there is any software or method I could use to achieve this? I am fairly new to Swift, so I am hoping this is possible.

How to create dragable view from top in SwiftUI?

I am trying to create dragable view from top which is kind of overlay .. I am attaching something same but having bottom sheet but i am looking for Top sheet. When overlay come background should be blur.
I am not sure about how to implement hence dont have code..I tried parent child for List in SwiftUI but that is having diff. Behaviour.
Any example or suggestion is appropriated.
something like this?
struct SheetView: View {
let title: String
var body: some View {
NavigationView {
List {
ForEach(0..<30) { item in
Text("Item \(item)")
}
}
.listStyle(.plain)
.navigationTitle(title)
}
}
}
struct ContentView: View {
#State private var currentDrag: CGFloat = 0
#State private var stateDrag: CGFloat = -300
let minDrag: CGFloat = -UIScreen.main.bounds.height + 120
var body: some View {
ZStack {
VStack(spacing: 0) {
Spacer(minLength: 36)
SheetView(title: "Bottom Sheet")
.background(.primary)
.opacity(stateDrag + currentDrag > minDrag ? 0.5 : 1)
.blur(radius: stateDrag + currentDrag > minDrag ? 5 : 0)
}
VStack(spacing: 0) {
SheetView(title: "Top Sheet")
.background(.background)
.offset(x: 0, y: stateDrag + currentDrag)
Image(systemName: "line.3.horizontal").foregroundColor(.secondary)
.padding()
.frame(maxWidth: .infinity)
.background(.background)
.contentShape(Rectangle())
.offset(x: 0, y: stateDrag + currentDrag)
.gesture(DragGesture()
.onChanged { value in
withAnimation {
currentDrag = value.translation.height
}
}
.onEnded { value in
stateDrag += value.translation.height
stateDrag = max(stateDrag, minDrag)
currentDrag = .zero
})
}
}
}
}

SwiftUI: Why isn't my view being shown off-screen as its frame suggests it should?

I have an interface where I want to be able to toggle a Side Menu and also a toolbar. The Side Menu slides in/out from the right and the Toolbar from the bottom.
I'm making it generic so it's reusable to whatever content one wants to use.
The code is this:
struct ControlsOverlayView<ToolbarContent: View, MenuContent: View>: View {
let menuWidth: CGFloat
let isMenuActive: Bool
let onMenuHide: () -> Void
let menuContent: MenuContent
let toolbarHeight: CGFloat
let isToolbarActive: Bool
let onToolbarHide: () -> Void
let toolbarContent: ToolbarContent
init(menuWidth: CGFloat = 270,
isMenuActive: Bool = true,
onMenuHide: #escaping () -> Void,
toolbarHeight: CGFloat = 44,
isToolbarActive: Bool = true,
onToolbarHide: #escaping () -> Void,
#ViewBuilder menuContent: () -> MenuContent,
#ViewBuilder toolbarContent: () -> ToolbarContent) {
self.menuWidth = menuWidth
self.isMenuActive = isMenuActive
self.onMenuHide = onMenuHide
self.menuContent = menuContent()
self.toolbarHeight = toolbarHeight
self.isToolbarActive = isToolbarActive
self.onToolbarHide = onToolbarHide
self.toolbarContent = toolbarContent()
}
var body: some View {
ZStack {
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.3))
.opacity(self.isMenuActive ? 1.0 : 0.0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.onMenuHide()
}
GeometryReader { geometry in
VStack {
HStack {
Spacer()
let space: CGFloat = 0.0
let offset = self.isMenuActive ? space : space + self.menuWidth
let toolbarHeight = isToolbarActive ? self.toolbarHeight : 0
self.menuContent
.frame(width: self.menuWidth, height: geometry.size.height - toolbarHeight, alignment: .center)
.background(Color.red)
.offset(x: offset)
.animation(.default)
}
let offset = self.isToolbarActive ? 0 : -self.toolbarHeight
self.toolbarContent
.frame(width: geometry.size.width,
height: self.toolbarHeight,
alignment: .center)
.background(Color.yellow)
.offset(y: offset)
.animation(.default)
}
}
}
}
}
struct ControlsOverlayView_Previews: PreviewProvider {
static var previews: some View {
ControlsOverlayView(menuWidth: 270,
isMenuActive: true,
onMenuHide: {},
toolbarHeight: 44,
isToolbarActive: false,
onToolbarHide: {}) {
Text("Menu Content")
} toolbarContent: {
Text("Toolbar Content")
}
}
}
The Problem: Given the preview settings, I don't think I should see the toolbar. And yet I do. Attached is a screenshot of the Canvas with the toolbarContent.frame(...) line highlighted in code. You can see that it shows a frame to be drawn offscreen, but the content is not drawn there with it.
I was following from the code to make the side menu slide in/out on the horizontal axis and thought I just need to do essentially the same for the toolbar, but as you can see, that approach doesn't work.
So I made it work, but I still have no idea why.
Using this new body code, I set the frame of the HStack, and adjusted the offset. Strange is that given the offset value of 'self.toolbarHeight / 2' I don't think it should yield the desired result, and yet it does:
var body: some View {
ZStack {
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.3))
.opacity(self.isMenuActive ? 1.0 : 0.0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.onMenuHide()
}
GeometryReader { geometry in
VStack {
let toolbarHeight = isToolbarActive ? self.toolbarHeight : 0
HStack {
Spacer()
let space: CGFloat = 0.0 //geometry.size.width - self.menuWidth
let offset = self.isMenuActive ? space : space + self.menuWidth
self.menuContent
.frame(width: self.menuWidth, height: geometry.size.height - toolbarHeight, alignment: .center)
.background(Color.red)
.offset(x: offset)
.animation(.default)
}
.frame(width: geometry.size.width,
height: geometry.size.height - toolbarHeight,
alignment: .center)
// Why does this work? You'd think it should be self.toolbarHeight without the factor of 2
let offset = self.isToolbarActive ? 0 : self.toolbarHeight/2
self.toolbarContent
.frame(width: geometry.size.width,
height: self.toolbarHeight,
alignment: .center)
.background(Color.yellow)
.offset(y: offset)
.animation(.default)
}
}
}
}

Custom Tab Bar SwiftUi [duplicate]

This question already has answers here:
iOS 14 SwiftUI Keyboard lifts view automatically
(4 answers)
Closed 2 years ago.
SwiftUi am learning new. I created a tab bar, but when the keyboard is turned on, the tabbar goes up with the keyboard.
How can I fix it. Can you help me?
I want to fix the tab bar to the bottom.
Or is there something that can get the keyboard to the top of the pages?
Since I did not know where to fix it, I had to throw out all the codes.
struct ContentView: View {
#State var selected = 0
var body: some View {
VStack{
if self.selected == 0{
Color.blue
}
else if self.selected == 1{
Color.red
}
else if self.selected == 2{
Color.pink
}
else if self.selected == 3{
Color.green
}
else if self.selected == 4{
Color.yellow
}
Spacer()
ZStack(alignment: .top){
BottomBar(selected: self.$selected)
.padding()
.padding(.horizontal, 22)
.background(CurvedShape())
Button(action: {
self.selected = 4
}) {
Image(systemName:"heart.fill").renderingMode(.original).padding(18)
}.background(Color.yellow)
.clipShape(Circle())
.offset(y: -32)
.shadow(radius: 5)
}
}.background(Color("Color").edgesIgnoringSafeArea(.top))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CurvedShape : View {
var body : some View{
Path{path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: UIScreen.main.bounds.width, y: 0))
path.addLine(to: CGPoint(x: UIScreen.main.bounds.width, y: 55))
path.addArc(center: CGPoint(x: UIScreen.main.bounds.width / 2, y: 55),
radius: 30, startAngle: .zero, endAngle: .init(degrees: 180), clockwise: true)
path.addLine(to: CGPoint(x: 0, y: 55))
}.fill(Color.white)
.rotationEffect(.init(degrees: 180))
}
}
struct BottomBar : View {
#Binding var selected : Int
var body : some View{
HStack{
Button(action: {
self.selected = 0
}) {
Image(systemName:"lasso")
}.foregroundColor(self.selected == 0 ? .black : .gray)
Spacer(minLength: 12)
Button(action: {
self.selected = 1
}) {
Image(systemName: "trash")
}.foregroundColor(self.selected == 1 ? .black : .gray)
Spacer().frame(width: 120)
Button(action: {
self.selected = 2
}) {
Image(systemName:"person")
}.foregroundColor(self.selected == 2 ? .black : .gray)
.offset(x: -10)
Spacer(minLength: 12)
Button(action: {
self.selected = 3
}) {
Image(systemName: "pencil")
}.foregroundColor(self.selected == 3 ? .black : .gray)
}
}
}
Set
.ignoresSafeArea(.keyboard)
To content view.

Resources