SwiftUI Horizontal Transition with Stack of Cards - swiftui

I have a stack of cards and I am trying to get a horizontal transition. The animation of the new card should fully slide in from the left. The disappearing card should slide out to the right.
import SwiftUI
struct ContentView: View {
#State var currentIndex = 0
var body: some View {
VStack{
Button("Next"){
withAnimation(.easeIn(duration: 2.0)){
self.currentIndex += 1
}
}
ZStack{
ForEach(0...100, id:\.self){i in
ZStack{
if self.currentIndex == i{
Card(text: String(i))
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
}
}
}
}
}
}
}
struct Card: View{
var text:String
var body: some View{
Text(text)
.frame(minWidth:0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.yellow)
.border(Color.purple, width: 5)
}
}

wrapping two ZStack with alignment .topand .leading solved it for me:
import SwiftUI
struct ContentView: View {
#State var currentIndex = 0
var body: some View {
VStack{
Button("Next"){
withAnimation(.easeIn(duration: 3.0)){
self.currentIndex += 1
}
}
ZStack(alignment:.leading){
ZStack(alignment:.top){
ForEach(0...100, id:\.self){i in
ZStack(){
if self.currentIndex == i{
Card(text: String(i))
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
}
}
}
}
}
}
}
}
struct Card: View{
var text:String
var body: some View{
Text(text)
.frame(minWidth:0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.yellow)
.border(Color.purple, width: 5)
}
}

Related

SwiftUI: Double picker wheels with system behavioral

I want to recreate system picker behavioral with two options in wheels with SwiftUI and faced ton of problem. Some of this I solved but some still unsolved. I have pop-ups with different views inside. One of the view it's a DatePicker with displayedComponents: .hourAndMinute. And other one is two Pickers inside HStack. My question is how to make Pickers make look like in system: without white spacing between?
struct MultyPicker: View {
#State var value = 1
#State var value2 = 1
var body: some View {
ZStack(alignment: .bottom) {
Color.black.opacity(0.5)
ZStack {
VStack {
Text("Header")
.font(.title3)
.fontWeight(.bold)
HStack(spacing: 0) {
Picker(selection: $value, label: Text("")) {
ForEach(1..<26) { number in
Text("\(number)")
.tag("\(number)")
}
}
.pickerStyle(WheelPickerStyle())
.compositingGroup()
.clipped(antialiased: true)
Picker(selection: $value2, label: Text("")) {
ForEach(25..<76) { number in
Text("\(number)")
.tag("\(number)")
}
}
.pickerStyle(WheelPickerStyle())
.compositingGroup()
.clipped(antialiased: true)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 34)
.foregroundColor(.white)
)
}
.padding(.horizontal)
.padding(.bottom, 50)
}
.edgesIgnoringSafeArea([.top, .horizontal])
}
}
// This extension for correct touching area
extension UIPickerView {
open override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: super.intrinsicContentSize.height)
}
}
Want to achive looks like that with one grey line in selected value
//
// Test2.swift
// Test
//
// Created by Serdar Onur KARADAĞ on 26.08.2022.
//
import SwiftUI
struct Test2: View {
#State var choice1 = 0
#State var choice2 = 0
var body: some View {
ZStack {
Rectangle()
.fill(.gray.opacity(0.2))
.cornerRadius(30)
.frame(width: 350, height: 400)
Rectangle()
.fill(.white.opacity(1))
.cornerRadius(30)
.frame(width: 300, height: 350)
VStack {
Text("HEADER")
HStack(spacing: 0) {
Picker(selection: $choice1, label: Text("C1")) {
ForEach(0..<10) { n in
Text("\(n)").tag(n)
}
}
.pickerStyle(.wheel)
.frame(minWidth: 0)
.clipped()
Picker(selection: $choice2, label: Text("C1")) {
ForEach(0..<10) { n in
Text("\(n)").tag(n)
}
}
.pickerStyle(.wheel)
.frame(minWidth: 0)
.clipped()
}
}
}
}
}
struct Test2_Previews: PreviewProvider {
static var previews: some View {
Test2()
}
}
SwiftUI multi-component Picker basically consists of several individual Picker views arranged horizontally. Therefore, we start by creating an ordinary Picker view for our first component. I am using Xcode version 13.4.1(iOS 15.0).
import SwiftUI
struct ContentView: View {
#State var hourSelect = 0
#State var minuteSelect = 0
var hours = [Int](0..<24)
var minutes = [Int](0..<60)
var body: some View {
ZStack {
Color.black
.opacity(0.5)
.ignoresSafeArea()
.preferredColorScheme(.light)
Rectangle()
.fill(.white.opacity(1))
.cornerRadius(30)
.frame(width: 300, height: 350)
VStack {
Text("Header")
HStack(spacing: 0) {
Picker(selection: $hourSelect, label: Text("")) {
ForEach(0..<self.hours.count) { index in
Text("\(self.hours[index])").tag(index)
}
}
.pickerStyle(.wheel)
.frame(minWidth: 0)
.compositingGroup()
.clipped()
Picker(selection: $minuteSelect, label: Text("")) {
ForEach(0..<self.minutes.count) { index in
Text("\(self.minutes[index])").tag(index)
}
}
.pickerStyle(.wheel)
.frame(minWidth: 0)
.compositingGroup()
.clipped()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Output :

No fullScreenCover animation on close button

I have a strange behavior.
When I close my fullScreenCover, I have no animation on it. On the other hand, I have the animation at the opening. I don't understand why I have no close animation.
Example codes
import SwiftUI
struct ContentView: View {
#EnvironmentObject var myViewModel: MyViewModel
private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]
var body: some View {
ScrollView {
LazyVGrid(columns: gridItemLayout, spacing: 10) {
ForEach(myViewModel.data, id: \.self) { data in
ChildView(data: data)
}
}
}
.onAppear() {
self.myViewModel.getData()
}
.navigationBarTitle("My ContentView")
}
}
import SwiftUI
struct ChildView: View {
#State var data: Data
#State var showModal = false
var body: some View {
Button(action: {
self.showModal.toggle()
}, label: {
VStack {
Text("\(data.name)")
.font(.body)
.frame(width: 180, height: 150/2, alignment: .bottom)
VStack {
Text("\(Image(systemName: "checkmark.bubble")) 15")
Text("\(Image(systemName: "clock")) 20'")
}
.frame(width: 180, height: 150/2, alignment: .bottomTrailing)
}
.cornerRadius(10)
.frame(minWidth: 0, maxWidth: 180, minHeight: 150, maxHeight: 150, alignment: .center)
})
.fullScreenCover(isPresented: $showModal) {
OtherView()
}
}
}
import SwiftUI
struct OtherView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
HStack {
Text("CLOSE")
Spacer()
Image(systemName: "checkmark")
.resizable()
.frame(width: 30, height: 30)
.padding()
}
}
}
.navigationBarTitle("My Modal", displayMode: .inline)
}
}
}
Thanks.

iOS 15 SwiftUI Conditionals on a view with Navigation View makes NavigationBar config to be ignore if navigationViewStyle stack

been searching for this everywhere and can't find anything around this, I believe is a bug, maybe is not.
I need NavigationView with .navigationViewStyle(.stack) to have it stacked on the iPad and make it look the same as the iphone, now suppose you have this view:
import SwiftUI
struct ContentView: View {
#State var isShowingProfile = false
#State var isNavigationViewShowing = true
var body: some View {
if isNavigationViewShowing {
NavigationView {
VStack {
Button("Simple view") {
isNavigationViewShowing = false
}
.padding()
Button("Profile navigation") {
isShowingProfile = true
}
.padding()
NavigationLink(
destination: ProfileView(),
isActive: $isShowingProfile
) {
EmptyView()
}
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
.background(Color.gray)
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
} else {
VStack {
Button("Show NavigationView"){
isNavigationViewShowing = true
}
.padding()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
).background(Color.yellow)
}
}
}
struct ProfileView: View {
var body: some View {
Text("This is a profile")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Well this just show this 3 simple views:
The navigationView when you start
The Profile view if you tap on "Profile navigation"
Finally the Simple view which is trigger by the conditional state pressing "Simple view"
Up to here is all fine and good.
The problem is when navigate to the "Simple view" and then tap "Show NavigationView" to navigate back to the NavigtionView.
The app opens the first view (NavigationView), but the NavigationView ignores the .navigationBarHidden(true) and just show a big empty space on the top. In fact, it would ignore things like .navigationBarTitleDisplayMode(.inline) and just show the large version of the navigationBar
This is working correctly in all iOS 14.x, but on iOS 15.0 seems broken. The behaviour continues to be the same on iOS 15.1 beta.
Any idea whats going on? I'm not really interested in changing the conditionals on the view, because real life app is more complex.
Also, I tried ViewBuilder without any success. And if I take out .navigationViewStyle(.stack) it works all fine on iOS 15, but then the view on the iPad is with the side menu.
Thanks a lot for any tip or help, you should be able to reproduce in simulator and real device.
Video of the explained above
I think the better solution all around is to not have the NavigationView be conditional. There is no reason your conditional can't just live in the NavigationView. You just don't ever want the bar to show. Therefore, this code would seem to meet the requirements:
struct ContentView: View {
#State var isShowingProfile = false
#State var isNavigationViewShowing = true
var body: some View {
NavigationView {
Group {
if isNavigationViewShowing {
VStack {
Button("Simple view") {
isNavigationViewShowing = false
}
.padding()
Button("Profile navigation") {
isShowingProfile = true
}
.padding()
NavigationLink(
destination: ProfileView(),
isActive: $isShowingProfile
) {
EmptyView()
}
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
.background(Color(UIColor.systemGray6))
} else {
VStack {
Button("Show NavigationView"){
isNavigationViewShowing = true
}
.padding()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
).background(Color.yellow)
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
}
}
I used Group simply to put the .navigationBarHidden(true) in the correct place so the code would compile.
Is this the behavior you are looking for?
import SwiftUI
struct ContentView: View {
#State private var isShowingProfile = false
#State private var showSimple = false
var body: some View {
NavigationView {
VStack {
Button("Simple view") {
showSimple = true
}
.padding()
Button("Profile navigation") {
isShowingProfile = true
}
.padding()
NavigationLink(destination: ProfileView(), isActive: $isShowingProfile) {
EmptyView()
}
}
.fullScreenCover(isPresented: $showSimple, onDismiss: {
print("Dismissed")
}, content: {
SimpleView()
})
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
.background(Color.gray)
.navigationBarHidden(true)
}
.navigationViewStyle(.stack)
}
}
struct ProfileView: View {
var body: some View {
Text("This is a profile")
}
}
struct SimpleView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button("Show NavigationView") {
presentationMode.wrappedValue.dismiss()
}
.padding()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
).background(Color.yellow)
}
}

Map Inside VStack Causing Crash

I have a simple Map view:
struct MapContainer: View {
#EnvironmentObject var store: AppStore
#State private var region: MKCoordinateRegion = Location.defaultRegion
var events: [Event] {
store.state.getEventsFromSearchResults()
}
func select(id: UUID) {}
var body: some View {
Map(
coordinateRegion: $region,
interactionModes: .all,
showsUserLocation: true,
annotationItems: events,
annotationContent: { event in
MapAnnotation(coordinate: event.coordinates) {
Image("MapMarker")
.resizable()
.scaledToFit()
.frame(width: 24)
.foregroundColor(
event.id == store.state.searchState.tapped
? Color("Accent")
: .black
)
.onTapGesture { select(id: event.id) }
}
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear { region = store.state.searchState.near.region }
}
}
And I have a screen that uses the above container:
struct MapScreen: View {
let height: CGFloat? = nil
var body: some View {
GeometryReader { geo2 in
ZStack(alignment: .top) {
VStack {
MapContainer()
.animation(.easeOut)
}
.frame(height: height != nil ? height : geo2.size.height / 2 + geo2.safeAreaInsets.top)
VStack(spacing: 0) {
VStack(spacing: 0) {
HStack {
Image(systemName: "minus")
.font(.system(size: 40))
.foregroundColor(Color("LightGray"))
.padding(.vertical, 10)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
MapList()
}
.frame(height: 500)
.background(Color.white.shadow(color: .gray, radius: 5, x: 0, y: 3))
}
.frame(maxHeight: .infinity, alignment: .bottom)
.animation(.easeOut)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.edgesIgnoringSafeArea(.vertical)
}
}
}
The above crashes with - Thread 1: EXC_BAD_ACCESS (code=1, address=0x38) - but if I assign a hard coded value to the MapContainer e.g.
MapContainer().frame(height: 400)
Then everything works fine. Anyone know what's causing the issue?

SwiftUI Tabbar persists navigationBarTitle position in tab's views

How can I stop a tab's scrollview's offset being affect by other tab's offset?
I don't want to force the scroll view to the top every time you show a new tab, but just want the new tabs to be not affected by the scroll position of the last tab I viewed.
import SwiftUI
enum Tab {
case First, Second, Third
var title: String {
switch self {
case .First:
return "First"
case .Second:
return "Second"
case .Third:
return "Third"
}
}
}
struct ContentView: View {
#State var selectedTab = Tab.First
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
FirstView()
.tabItem {
Text("First")
}.tag(Tab.First)
SecondView()
.tabItem {
Text("Second")
}.tag(Tab.Second)
ThirdView()
.tabItem {
Text("Third")
}.tag(Tab.Third)
}.navigationBarTitle(selectedTab.title, displayMode: .automatic)
.navigationBarHidden(false)
}
}
}
struct FirstView: View {
let data = [1,2,3,4,5,6,7,8,9,10]
var body: some View {
ScrollView(showsIndicators: true) {
VStack {
ForEach(data, id: \.self) { item in
Text("\(item)")
.frame(minWidth: 0, idealWidth: 100, maxWidth: .infinity, minHeight: 0, idealHeight: 100, maxHeight: .infinity, alignment: .center)
}
}
}
}
}
struct SecondView: View {
let data = [1,2,3,4,5,6,7,8,9,10]
var body: some View {
ScrollView(showsIndicators: true) {
VStack {
ForEach(data, id: \.self) { item in
Text("\(item)")
.frame(minWidth: 0, idealWidth: 100, maxWidth: .infinity, minHeight: 0, idealHeight: 100, maxHeight: .infinity, alignment: .center)
}
}
}
}
}
struct ThirdView: View {
let data = [1,2,3,4,5,6,7,8,9,10]
var body: some View {
ScrollView(showsIndicators: true) {
VStack {
ForEach(data, id: \.self) { item in
Text("\(item)")
.frame(minWidth: 0, idealWidth: 100, maxWidth: .infinity, minHeight: 0, idealHeight: 100, maxHeight: .infinity, alignment: .center)
}
}
}
}
}
It is because you use one NavigationView, so it preserves own state. Make NavigationView independent for each tab.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var selectedTab = Tab.First
var body: some View {
TabView(selection: $selectedTab) {
NavigationView {
FirstView()
.navigationBarTitle(Tab.First.title)
}
.tabItem {
Text("First")
}.tag(Tab.First)
NavigationView {
SecondView()
.navigationBarTitle(Tab.Second.title)
}
.tabItem {
Text("Second")
}.tag(Tab.Second)
NavigationView {
ThirdView()
.navigationBarTitle(Tab.Third.title)
}
.tabItem {
Text("Third")
}.tag(Tab.Third)
}
}
}