Animate view to fullscreen (Card to detail) - swift

Im trying to replicate the cards from the Appstore's today tab.
This is what I have so far:
struct ContentView: View {
#State var showDetail = false
#State var selectedForDetail : Post?
var posts = [...] // Just sample data: Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor...") etc.
var body: some View {
ZStack {
ScrollView{
ForEach(self.posts){ current in
PostView(post: current, isDetailed: self.$showDetail).onTapGesture {
self.selectedForDetail = current
withAnimation(.spring()){
self.showDetail.toggle()
}
}
}
}
if showDetail {
PostView(post: selectedForDetail!, isDetailed: self.$showDetail).onTapGesture {
withAnimation(.spring()){
self.showDetail.toggle()
}
}
}
}
}
}
struct PostView : View {
var post : Post
#Binding var isDetailed : Bool
var body : some View {
VStack(alignment: .leading){
HStack(alignment: .top){
Text(post.subtitle)
Spacer()
}.padding([.top, .horizontal])
Text(post.title).padding([.horizontal, .bottom])
if isDetailed {
Text(post.extra).padding([.horizontal, .bottom])
Spacer()
}
}
.background(isDetailed ? Color.green : Color.white)
.cornerRadius(isDetailed ? 0 : 16)
.shadow(radius: isDetailed ? 0 : 12)
.padding(isDetailed ? [] : [.top, .horizontal])
.edgesIgnoringSafeArea(.all)
}
}
struct Post : Identifiable {
var id = UUID()
var subtitle : String
var title : String
var extra : String
}
It works so far that pressing a PostView shows a detailed PostView in fullscreen. But the animation looks way off. I also tried to follow these video tutorials:
https://www.youtube.com/watch?v=wOQWAzsKi4U
https://www.youtube.com/watch?v=8gDtf22TwW0
But these only worked with static content (and no ScrollView. Using one results in overlapped PostViews) or didn't look right..
So my question is how can i improve my code to get as close as possible to the todays tab in die Appstore? Is my approach even feasible?
Thanks in advance

Please find below your code modified to fit your needs
import SwiftUI
struct ContentView: View {
#State var selectedForDetail : Post?
#State var showDetails: Bool = false
// Posts need to be #State so changes can be observed
#State var posts = [
Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor..."),
Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor..."),
Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor..."),
Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor..."),
Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor...")
]
var body: some View {
ScrollView {
VStack {
ForEach(self.posts.indices) { index in
GeometryReader { reader in
PostView(post: self.$posts[index], isDetailed: self.$showDetails)
.offset(y: self.posts[index].showDetails ? -reader.frame(in: .global).minY : 0)
.onTapGesture {
if !self.posts[index].showDetails {
self.posts[index].showDetails.toggle()
self.showDetails.toggle()
}
}
// Change this animation to what you please, or change the numbers around. It's just a preference.
.animation(.spring(response: 0.6, dampingFraction: 0.6, blendDuration: 0))
// If there is one view expanded then hide all other views that are not
.opacity(self.showDetails ? (self.posts[index].showDetails ? 1 : 0) : 1)
}
.frame(height: self.posts[index].showDetails ? UIScreen.main.bounds.height : 100, alignment: .center)
.simultaneousGesture(
// 500 will disable ScrollView effect
DragGesture(minimumDistance: self.posts[index].showDetails ? 0 : 500)
)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct PostView : View {
#Binding var post : Post
#Binding var isDetailed : Bool
var body : some View {
VStack(alignment: .leading){
HStack(alignment: .top){
Text(post.subtitle)
Spacer()
// Only show close button if page is showing in full screen
if(self.isDetailed) {
// Close Button
Button(action: {
self.post.showDetails.toggle()
self.isDetailed.toggle()
}) {
Text("X")
.frame(width: 48, height: 48, alignment: .center)
.background(Color.white)
.clipShape(Circle())
}.buttonStyle(PlainButtonStyle())
}
}.padding([.top, .horizontal])
Text(post.title).padding([.horizontal, .bottom])
if isDetailed {
Text(post.extra).padding([.horizontal, .bottom])
Spacer()
}
}
.background(isDetailed ? Color.green : Color.white)
.cornerRadius(isDetailed ? 0 : 16)
.shadow(radius: isDetailed ? 0 : 12)
.padding(isDetailed ? [] : [.top, .horizontal])
.edgesIgnoringSafeArea(.all)
}
}
struct Post : Identifiable {
var id = UUID()
var subtitle : String
var title : String
var extra : String
var showDetails: Bool = false // We need this variable to control each cell individually
}
If any explanation is needed please let me know.
Note: I added a showDetails property to your Post model, this is needed to control individual cells. Keep in mind best practice is to separate that into a different array to take care of whats visible and what not but this will do for now.
Also note we are looping through indices of our array and not the objects, this way we have flexibility of choosing what to show.

Related

How can i make an individual button for each user so when its pressed it toggles the isFollowingUser Bool and changes to following?

Currently since its in a ForEach loop and i only have one State var, when i press follow they all change to following. I used a ternary operator to make the button color and text switch if the isFollowing is toggled. How would i go about making it toggle only for a specified user when the button is clicked. Would i just need to make 3 buttons outside the loop? When i used user.isFollowingUser.toggle in the button it tells me that I cant mutate user.
import SwiftUI
struct InstagramModel: Identifiable{
let id: String = UUID().uuidString
let username: String
let userImage: String
let followers: Int
let isVerified: Bool
let isFollowingUser: Bool
}
struct ModelPractice: View {
#State private var users: [InstagramModel] = [
InstagramModel(username: "aleedagenie", userImage: "AliImage", followers: 490, isVerified: true, isFollowingUser: false),
InstagramModel(username: "nicole29", userImage: "volcano2", followers: 1090, isVerified: true, isFollowingUser: false),
InstagramModel(username: "shamat81", userImage: "crashedCar", followers: 290, isVerified: false, isFollowingUser: false)
]
#State private var isFollowing: Bool = false
#State private var isShowDialog: Bool = false
#State private var background: Color = .mint
var body: some View {
NavigationView{
VStack{
List{
Section {
ForEach(users) { user in
VStack(alignment: .leading) {
HStack {
Image(user.userImage)
.resizable()
.frame(width: 35, height: 35)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(user.username)
.font(.headline)
HStack {
Text("Followers:")
.font(.caption)
Text("\(user.followers)")
.font(.caption)
}
}
Spacer()
if user.isVerified{
Image(systemName: "checkmark.seal.fill")
.foregroundColor(.blue)
}
}
Button {
isFollowing.toggle()
} label: {
Text(isFollowing ? "Following" : "Follow")
.foregroundColor(isFollowing ? .black: .white)
.frame(maxWidth: 90)
.background(isFollowing ? .white: .blue)
.cornerRadius(12)
}
.padding(.horizontal, 44)
}
}
} header: {
Text("Instagram Users")
}
.listRowBackground(background)
}
Button {
isShowDialog.toggle()
} label: {
Text("Change Page Style")
.bold()
.frame(maxWidth: 140)
.background(.orange)
.cornerRadius(20)
}
.confirmationDialog("Text", isPresented: $isShowDialog, actions: {
Button {
background = .yellow
} label: {
Text("Option 1")
}
Button {
background = .gray
} label: {
Text("Option 2")
}
Button {
background = .green
} label: {
Text("Option 3")
}
})
.navigationTitle("Instagram")
}
}
}
}
struct ModelPractice_Previews: PreviewProvider {
static var previews: some View {
ModelPractice()
}
}
The problem here is you are trying to mutate the variable of closure called user. user is a temporary variable which is not linked with your users, so you can't mutate it.
Instead you should mutate the users.
Here is my demo, try it out. Code is below the image:
struct UserModel: Identifiable {
var id = UUID()
var name: String = ""
var following = false
}
struct DemoView: View {
#State var listUser = [
UserModel(name: "Lamb Chop", following: false),
UserModel(name: "Steak", following: false)
]
var body: some View {
List {
ForEach(listUser.indices) { index in
HStack {
Text(listUser[index].name)
Button {
listUser[index].following.toggle()
} label: {
Text(listUser[index].following ? "following" : "follow")
}
.padding(5)
.background(.black)
.cornerRadius(15)
}
}
}
}
}

White rectangular space in chat view

I have simple chat app, and when I run my project there is white rectangular like image. I check may times my code, I did not detect any error. when I add toolbar, this rectangular padding bottom, any idea?
Screenshot:
ChatView:
struct chatList : View {
var body : some View{
ScrollView(.vertical, showsIndicators: false) {
VStack{
ForEach(Eachmsg){i in
chatCell(data: i)
}
}
}.padding(.horizontal, 15)
.background(Color.white)
}}
struct chatCell : View {
var data : msgdataType
var body : some View{
HStack{
if data.myMsg{
Spacer()
Text(data.msg)
.padding()
.background(Color("color"))
.clipShape(msgTail(mymsg: data.myMsg))
.foregroundColor(.white)
}
else{
Text(data.msg)
.padding()
.background(Color("gray"))
.clipShape(msgTail(mymsg: data.myMsg))
Spacer()
}
}.padding(data.myMsg ? .leading : .trailing, 55)
.padding(.vertical,10)
}
}
msgdataType:
struct msgdataType : Identifiable {
var id : Int
var msg : String
var myMsg : Bool
}
Eachmsg:
var Eachmsg = [
msgdataType(id: 0, msg: "New Album Is Going To Be Released!!!!",
myMsg: false),.....
In your chatCell, the blank space showing is for message with myMsg=true
if data.myMsg {
Spacer()
Text(data.msg)
.padding()
.background(Color("color"))
.clipShape(msgTail(mymsg: data.myMsg))
.foregroundColor(.white)
}
Try changing foregroundColor to .red or .blue or change background to valid value and you will see the message.

Why My second view cannot jump back to the root view properly

My App currently has two pages, first page has a circle plus button which could lead us to a second page. Basically, I have a save button which after clicking it, we could get back to the rood page. I followed this link for going back to root view. I tried the most up voted code, his code works perfectly. I reduced his code to two scene (basically the same scenario as mine), which also works perfectly. But then I don't know why my own code, pasted below, doesn't work. Basically my way of handling going back to root view is the same as the one in the link.
//
// ContentView.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/22/20.
//
import SwiftUI
import UIKit
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
struct ContentView: View {
#EnvironmentObject private var fridge : Fridge
private var dbStartWith=0;
#State var pushed: Bool = false
#State private var selection = 1;
#State private var addFood = false;
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(1)
Text("random tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("profile")
}
.tag(0)
}.environmentObject(fridge)
}
}
struct FoodView: View{
var body: some View{
NavigationView{
Text("food destination view ");
}
}
}
struct AddFoodView: View{
#Binding var pushed : Bool
#EnvironmentObject private var fridgeView : Fridge
#State private var name = ""
#State private var count : Int = 1
#State private var category : String = "肉类";
#State var showCategory = false
#State var showCount = false
var someNumberProxy: Binding<String> {
Binding<String>(
get: { String(format: "%d", Int(self.count)) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.count = value.intValue;
}
}
)
}
var body: some View{
ZStack{
NavigationView{
VStack{
Button (action: {
self.pushed = false ;
//let tempFood=Food(id: fridgeView.index!,name: name, count: count, category: category);
//fridgeView.addFood(food: tempFood);
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
ZStack{
if self.showCount{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCount=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $count,label: EmptyView()) {
ForEach(1..<100){ number in
Text("\(number)").tag("\(number)")
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
.offset(x:10,y:-10)
Spacer()
}
if self.showCategory{
let categoryArr = ["肉类","蔬菜类","饮料类","调味品类"]
ZStack{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCategory=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $category,label: EmptyView()) {
ForEach(0..<categoryArr.count){ number in
Text(categoryArr[number]).tag(categoryArr[number])
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
Spacer()
}.offset(x:10,y:20)
}
}
}.animation(.easeInOut)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you read my code carefully, there are some variables are missing referencing. That's because I pasted part of the code that relates to my issue.
Food Class
//
// Food.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/23/20.
//
import Foundation
class Food: Identifiable {
init(id:Int, name: String, count: Int, category: String){
self.id=id;
self.name=name;
self.count=count;
self.category=category;
}
var id: Int
var name: String
var count: Int
var category: String
}
Fridge class
//
// Fridge.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/27/20.
//
import Foundation
class Fridge: ObservableObject{
init(){
db=DBhelper();
let result = setIndex(database: db!);
self.index = result.1;
self.container=result.0;
}
var db:DBhelper?
var index : Int?
#Published var container : [Food]?;
func setIndex(database: DBhelper) -> ([Food],Int){
let foodList : [Food] = database.read();
var index=0;
for food in foodList{
index = max(food.id,index);
}
return (foodList,(index+1));
}
func updateindex(index: inout Int){
index=index+1;
}
func testExist(){
if let data = db {
print("hi")
}
else{
print("doesnt exist")
}
}
func addFood(food:Food){
self.db!.insert(id: self.index!, name: food.name, count:food.count, category: food.category);
self.container!.append(food);
}
}
Because you implemented a new NaviagtionView in AddFoodView. Simply remove this and it should work. Look at the link you provided. There is no NavigationView in the child.
Correct me if Im wrong but the core code parts here that produce this issue are as follows:
Here you start:
struct ContentView: View {
#State var pushed: Bool = false
// Deleted other vars
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
// Here you navigate to the child view
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
Here you land and want to go back to root:
struct AddFoodView: View{
#Binding var pushed : Bool
// Deleted the other vars for better view
var body: some View{
ZStack{
NavigationView{ // <-- remove this
VStack{
Button (action: {
// here you'd like to go back
self.pushed = false;
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
For the future:
I have the feeling you might have troubles with the navigation in general.
Actually it is really simple:
You implement one NavigationView at the "root" / start of your navigation.
From there on you only use NavigationLinks to go further down to child pages. No NavigationView needed anymore.

SwiftUI: Initial list element's position is wrong

Im new to Swift and I have a question regarding the list view.
The code below creates a List of an image as a button followed by a text.
The problem is that the List elements are initially slightly left, but they move to the right into place when they're first updated.
import SwiftUI
struct TodoItem {
var name: String
var completed: Bool
init(_ name: String) {
self.name = name
self.completed = false
}
}
struct ContentView: View {
#State var todos: [TodoItem] = [
TodoItem("This"),
TodoItem("Is"),
TodoItem("Some"),
TodoItem("Todo"),
TodoItem("Task")
]
var body: some View {
NavigationView {
List(todos.indices) { index in
HStack {
Image(systemName: self.todos[index].completed ? "checkmark.circle" : "circle")
.imageScale(.large)
.onTapGesture {
self.todos[index].completed.toggle()
}
Text(self.todos[index].name)
Spacer()
}
}
.navigationBarTitle("Todos")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is fixed body (tested & works with Xcode 11.3.1)
var body: some View {
NavigationView {
List {
ForEach (todos.indices) { index in
HStack {
Image(systemName: self.todos[index].completed ? "checkmark.circle" : "circle")
.imageScale(.large)
.onTapGesture {
self.todos[index].completed.toggle()
}
Text(self.todos[index].name)
Spacer()
}
}.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
}
.navigationBarTitle("Todos")
}
}

Background of button not conditionally rendering

Essentially I have a button when pressed I want the background to become a different color. In order to do this I have an object that I alter, I have printed out the value of the Bool value in the object and see its changing but the color of the button is not changing.
Object With Bool:
class dummyObject: Identifiable, ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var id = UUID()
var isSelected: Bool {
willSet {
objectWillChange.send()
}
}
init(isSelected:Bool) {
self.isSelected = isSelected
}
}
View:
struct SelectionView: View {
var objs: [dummyObject] = [
dummyObject.init(isSelected: false)
]
var body: some View {
HStack{
ForEach(objs) { obj in
Button(action: {
obj.isSelected.toggle()
print("\(obj.isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(obj.isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(obj.isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}.frame(minWidth: 0, maxWidth: .infinity)
.padding()
}
}
Extract your Button into other view, where obj is #ObservedObject and everything will work:
import SwiftUI
import Combine
class dummyObject: Identifiable, ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var id = UUID()
var isSelected: Bool {
willSet {
objectWillChange.send()
}
}
init(isSelected:Bool) {
self.isSelected = isSelected
}
}
struct SelectionView: View {
var objs: [dummyObject] = [dummyObject.init(isSelected: false)]
var body: some View {
HStack{
ForEach(objs) { obj in
ObjectButton(obj: obj)
}
}
}
}
struct ObjectButton: View {
#ObservedObject var obj: dummyObject
var body: some View {
Button(action: {
self.obj.isSelected.toggle()
print("\(self.obj.isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(obj.isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(obj.isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}
struct SelectionView_Previews: PreviewProvider {
static var previews: some View {
SelectionView()
}
}
Here is modified your snapshot of code that works. Tested with Xcode 11.2 / iOS 13.2.
The main idea is made a model as value-type, so modifications of properties modify model itself, and introducing #State for view would refresh on changes.
struct dummyObject: Identifiable, Hashable {
var id = UUID()
var isSelected: Bool
}
struct SelectionView: View {
#State var objs: [dummyObject] = [
dummyObject(isSelected: false)
]
var body: some View {
HStack{
ForEach(Array(objs.enumerated()), id: \.element) { (i, _) in
Button(action: {
self.objs[i].isSelected.toggle()
print("\(self.objs[i].isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(self.objs[i].isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(self.objs[i].isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}.frame(minWidth: 0, maxWidth: .infinity)
.padding()
}
}