So I was reading about the Navigation in the Apple UI guidlines. In SwiftUI, for hierarchical navigation we use the NavigationLink-View. Flat navigation is for example the view for each song in apple music.
My questions:
I have a solution but I wonder what is the best practice solution.
Furthermore, I have to add the animation myself, whereas with NavigationLink I get it for "free". Why does the transition not work? I'm trying to get a similar animation as in apple music when skipping a song.
import SwiftUI
struct Song:Identifiable{
var id = UUID()
var name:String
var cover = Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1))
}
struct ContentView: View {
var songs:[Song] = []
init(){
for i in 0...1000{
self.songs.append(Song(name: "Song \(i)"))
}
}
#State var index = 0
var body: some View {
VStack{
DetailView(song:songs[index])
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
SongNavigation(index:$index, totalSongs: songs.count)
}.padding()
}
}
struct DetailView: View{
var song:Song
var body: some View{
VStack{
Text(song.name)
}.frame(minWidth:0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity).background(song.cover)
}
}
struct SongNavigation:View{
#Binding var index:Int
var totalSongs:Int
var body: some View{
HStack{
Button("previous"){
withAnimation(){
if self.index > 0{
self.index -= 1
}
}
}
Spacer()
Button("next"){
withAnimation(){
if self.index != self.totalSongs-1 {
self.index += 1
}
}
}
}
}
}
Related
Model
I have different variables in Travel.
import Foundation
struct Travel: Identifiable, Codable {
var id = UUID()
var name: String
var date = Date()
var location: String
var isFav: Bool
var score: Float
var comment: String
}
View model
I load and save data with UserDefaults. Always its work but in this model not.
import Foundation
class TravelViewModel: ObservableObject {
#Published var travelList = [Travel] ()
#Published var travelled = 0
init(){
load()
}
func load() {
guard let data = UserDefaults.standard.data(forKey: "travelList"),
let savedTravels = try? JSONDecoder().decode([Travel].self, from: data) else { travelList = []; return }
travelList = savedTravels
}
func save() {
do {
let data = try JSONEncoder().encode(travelList)
UserDefaults.standard.set(data, forKey: "travelList")
} catch {
print(error)
}
}
}
Adding Item View
I have addItems func and use this func in addItem button.
import SwiftUI
struct AddTravelView: View {
#StateObject var VM = TravelViewModel()
#State var name = ""
#State var location = ""
#State var isFav = false
#State var score = 0.00
#State var comment = ""
var body: some View {
VStack {
ZStack {
Rectangle()
.fill(.black.opacity(0.2))
.cornerRadius(20)
.frame(width: 350, height: 350)
VStack{
HStack {
Text("Name:")
.font(.system(size: 16 , weight: .medium))
TextField("Type..", text: $name)
}
HStack {
Text("Location:")
.font(.system(size: 16 , weight: .medium))
TextField("Type..", text: $location)
}
HStack {
Text("Score: \(Int(score))")
.font(.system(size: 16 , weight: .medium))
Slider(value: $score, in: 0...10, step: 1)
}
Spacer()
ZStack(alignment: .topLeading) {
Rectangle()
.fill(.white)
.cornerRadius(20)
.frame(height: 200)
VStack(alignment: .leading) {
TextField("Comment...", text: $comment, axis: .vertical)
}.padding()
}
}
.padding()
.frame(width: 300, height: 200)
}
Button {
addTravel()
} label: {
ZStack{
Rectangle()
.fill(.black.opacity(0.2))
.cornerRadius(20)
.frame(width: 350 , height: 100)
Text("ADD TRAVEL")
.font(.system(size: 25, weight: .medium, design: .monospaced))
.foregroundColor(.black)
}.padding()
}
}
}
func addTravel(){
VM.travelList.append(Travel(name: name, location: location, isFav: isFav, score: Float(score), comment: comment))
}
}
struct AddTravelView_Previews: PreviewProvider {
static var previews: some View {
AddTravelView()
}
}
Recent Adds view
In this page i wanna see Items i add before
import SwiftUI
struct RecentTravels: View {
#StateObject var VM = TravelViewModel()
var body: some View {
VStack {
ForEach(VM.travelList) {Travel in
HStack{
Image(systemName: "questionmark")
.frame(width: 50, height: 50)
.padding()
.overlay {
RoundedRectangle(cornerRadius: 5)
.stroke(.black, lineWidth: 2)
}
VStack(alignment: .leading) {
Text(Travel.name)
.font(.subheadline)
.bold()
.lineLimit(1)
Text("\(Travel.date)")
.font(.footnote)
.opacity(0.9)
.lineLimit(1)
}
Spacer()
VStack {
Image(systemName: "heart")
Spacer()
Text("\(Travel.score)")
}
.frame(height: 50)
.font(.system(size: 22))
}
}
}
}
}
struct RecentTravels_Previews: PreviewProvider {
static var previews: some View {
RecentTravels()
}
}
And ContentView
and calling those 2 views in ContentView.
import SwiftUI
struct ContentView: View {
#StateObject var VM = TravelViewModel()
var body: some View {
VStack {
AddTravelView()
RecentTravels()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When i write the all code in only ContentView is work but when i call another pages its not work. Usually it was work when i presss add item button and restart app but now its nothing. Its not work even restart the app.
The problem you have is that you have multiple VM, that have no relations to each other. You must not have more than one source of truth
in #StateObject var VM = TravelViewModel().
Keep the one you have in ContentView,
and pass it to the other view like this:
VStack {....}.environmentObject(VM).
In your AddTravelView and RecentTravels ,
add #EnvironmentObject var VM: TravelViewModel instead of #StateObject var VM = TravelViewModel().
Have a look at this link, it gives you some good examples of how to manage data in your app:
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
Is possible to get button itself in action closure with SwiftUI ?
struct ContentView: View {
var body: some View {
Button("test") {
// change the color of button when user is tapping on it
}
}
}
If you want to access the properties of your button you can create a custom ButtonStyle.
Use its configuration to set the desired behaviour.
struct CustomButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [.red, .orange]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.scaleEffect(configuration.isPressed ? 0.9 : 1)
}
}
The above example was taken from here: SwiftUI Tip: ButtonStyle and Animated Buttons
In your case you can adapt it to set a custom background:
.background(configuration.isPressed ? Color.red : Color.blue)
Here's how:
struct ContentView: View {
#State var color = Color.red
var body: some View {
Button("test") {
self.color = Color.green
}.background(color)
}
}
Ya... I just figure out by myself too
struct ContentView: View {
#State var buttonColor: Color = Color.clear
var body: some View {
Button(action: {
self.buttonColor = Color(red: Double.random(in: 0...1),
green: Double.random(in: 0...1),
blue: Double.random(in: 0...1))
}, label: {
Text("Button").background(buttonColor)
})
}
}
I am trying to move a view when it is being tapped. But unfortunately nothing happens. How do I toggle the position of it using the offset y axis?
struct Test: View {
#State var cards : [Card] = [Card(), Card(), Card(), Card(), Card(), Card()]
var body: some View {
VStack {
ZStack (alignment: .top){
ForEach (0..<cards.count) { i in
RoundedRectangle(cornerRadius: 10).frame(width: 250, height: 150)
.foregroundColor(Color.random).offset(y: self.cards[i].isFocus ? CGFloat(500) : CGFloat(i*50))
.zIndex(Double(i))
.onTapGesture {
self.cards[i].isFocus.toggle() // now the touched Card should move to an offset of y: 500, but nothing happens
}
}
}
Spacer()
}
}
}
struct Card :Identifiable{
#State var isFocus :Bool = false
var id = UUID()
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
extension Color {
static var random: Color {
return Color(red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1))
}
}
I see your code and I am sorry to say but its wrong in many aspects. I would recommend you to go through SwiftUI basics and understand how and when we use #State, #ObservedObject, #Binding etc. Also always try to break your Views into the smallest components.
Below is the code which I think fulfills your requirement.
struct Test: View {
var cards : [Card] = [Card(), Card(), Card(), Card(), Card(), Card()]
var body: some View {
VStack {
ZStack (alignment: .top){
ForEach (0..<cards.count) { i in
CardView(card: self.cards[i], i: i)
}
}
Spacer()
}
}
}
struct CardView: View {
#ObservedObject var card: Card
let i: Int
var body: some View {
RoundedRectangle(cornerRadius: 10).frame(width: 250, height: 150)
.foregroundColor(Color.random)
.offset(y: card.isFocus ? CGFloat(500) : CGFloat(i*10))
.zIndex(Double(i))
.onTapGesture {
withAnimation {
self.card.isFocus.toggle()
}
}
}
}
class Card: ObservableObject{
#Published var isFocus: Bool = false
var id = UUID()
}
I have control with an edit and a list in a Stack. I want the list to drop down from top to bottom to animate like a fancy menu. However I am having a few issues. If I just try to use the move transition nothing happens. If I use the scale transition it always scales form center never from top down. This was just trying to get the transition overridden to slide. Anything but fade.
My control looks like so
struct Search: Identifiable {
let id: UUID
let text: String
}
struct SearchBox: View {
#State var searchParam: String = ""
#State var stuff = [Search]()
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
}
var body: some View {
var binding = Binding<String>(
get: {
self.searchParam
},
set: {
self.stuff.append(
Search(id: UUID(), text: $0))
self.searchParam = $0
})
return VStack(spacing: 0.0) {
TextField("Search", text: binding )
.font(.title)
.padding()
.background(Color.white)
Color(.darkGray)
.frame(height: 1.0)
if stuff.count > 0 {
List(stuff, id: \.id) {
Text($0.text)
}
.transition(.slide)
}
}
}
struct SearchBox_Preview: PreviewProvider {
static var previews: some View{
SearchBox()
}
}
}
The content view is simple..
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .topLeading) {
Color.blue
SearchBox()
.frame(width: geometry.size.width * 0.40, alignment: .topLeading)
.frame(minHeight: 0, maxHeight: geometry.size.height * 0.40,
alignment: .topLeading)
.padding()
.clipped()
.shadow(radius: 5.0)
}
.background(Color.clear)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have to be missing, or not getting, something simple
Answered my own question. We need to force an animation by using withAnimation on a state. So I have changed my biding to be like so:
struct Search: Identifiable {
let id: UUID
let text: String
}
struct SearchBox: View {
#State var searchParam: String = ""
#State var stuff = [Search]()
#State var showList = false
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
}
var body: some View {
var binding = Binding<String>(
get: {
self.searchParam
},
set: {
self.stuff.append(
Search(id: UUID(), text: $0)
)
self.searchParam = $0
// change visibility state with in animation block.
withAnimation { self.showList = stuff.count > 0 }
})
return VStack(spacing: 0.0) {
TextField("Search", text: binding )
.font(.title)
.padding()
.background(Color.white)
Color(.darkGray)
.frame(height: 1.0)
if showList {
List(stuff, id: \.id) {
Text($0.text)
}
.transition(.slide)
}
}
}
struct SearchBox_Preview: PreviewProvider {
static var previews: some View{
SearchBox()
}
}
}
I am trying to update the list to reflect the new item when it is added. Currently, it isn't showing once I press the "paperplane" button. I have left out the repeating blocks of code if you see the references.
I have a suspicion that the problem comes in the button from MergedView but couldn't find a solution. It compiles but doesn't execute the intended action.
import Foundation
class One: Identifiable, ObservableObject { // ObservableObject requires class
let id: UUID
var item: String = ""
#Published var completed: Bool = false // this will affect the UI
init(item: String, completed: Bool) {
id = UUID()
self.item = item
self.completed = completed
}
}
class OneList: ObservableObject {
#Published var items = [One(item:"",completed:false)]
}
struct VitalRow: View {
#EnvironmentObject var item: One
var body: some View {
HStack{
Image(systemName: item.completed ? "checkmark.circle" : "circle")
Text(item.item)
}
}
}
struct ButtonView: View {
#EnvironmentObject var itemss1: OneList
#EnvironmentObject var itemss2: TwoList
#EnvironmentObject var itemss3: ThreeList
#EnvironmentObject var itemss4: FourList
#State private var showingAdditem: Bool = false
var body: some View {
VStack{
Spacer()
HStack{
Spacer()
Button(action:{
self.showingAdditem = true
}){
Image(systemName: "plus")
.font(Font.system(size: 25,weight: .regular))
.font(.largeTitle)
.frame( width: 50, height: 50)
.foregroundColor(.white)
}.sheet(isPresented: $showingAdditem)
{
MergedView()
.environmentObject(OneList())
.environmentObject(TwoList())
.environmentObject(ThreeList())
.environmentObject(FourList())
//items1: self.itemss1, items2: self.itemss2, items3: self.itemss3, items4: self.itemss4)
}.background(Color.blue)
.cornerRadius(25)
.shadow(color: Color.black.opacity(0.3), radius: 3,
x: 3,
y: 3)
.padding()
.offset(x: -5,
y: 15)
}
}
}
}
struct MergedView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var items1: OneList
#EnvironmentObject var items2: TwoList
#EnvironmentObject var items3: ThreeList
#EnvironmentObject var items4: FourList
/*#ObservedObject var items1: OneList
#ObservedObject var items2: TwoList
#ObservedObject var items3: ThreeList
#ObservedObject var items4: FourList*/
#State private var item = ""
#State var blockcolor1 = Color.red.opacity(0.90)
#State var blockcolor2 = Color.red.opacity(0.70)
#State var blockcolor3 = Color.red.opacity(0.45)
#State var blockcolor4 = Color.red.opacity(0.20)
#State var itemvalue: Int = 0
var body: some View {
GeometryReader { geo in
ZStack{
NavigationView{
HStack{
TextField("Item", text: self.$item)
.padding()
Button(action:{
if self.itemvalue == 1{
let things1 = One(item: self.item, completed: false)
self.items1.items.append(things1)
self.presentationMode.wrappedValue.dismiss()
}
{
Image(systemName:"paperplane")
.font(.headline)
.padding()
}
HStack(spacing: 10) {
Rectangle()
.fill(self.blockcolor1)
//.fill(Color.red.opacity(0.90))
//.frame(width: 140,height: 180)
.frame(width: geo.size.width/3, height: geo.size.height/4.6)
.cornerRadius(15)
.shadow(radius: 6)
.onTapGesture{
self.blockcolor1 = Color.red.opacity(0.1)
self.itemvalue = 1
}
struct CheckboxList: View {
#EnvironmentObject var item1List: OneList
#EnvironmentObject var item2List: TwoList
#EnvironmentObject var item3List: ThreeList
#EnvironmentObject var item4List: FourList
var body: some View {
NavigationView{
List {
Section(header: Text("Vital")) {
ForEach(item1List.items.indices) { index in
VitalRow()
.environmentObject(self.item1List.items[index])
.onTapGesture {
self.item1List.items[index].completed.toggle()
}
}
}
}
Thank you in advance.