I am trying to animate the contents of a list of points. The list is sorted from highest to lowest, so it reorganizes as lower values are higher than the row above. The animation works great by using .animation(.default) on the list, however, it also animates the entire list when I open the view. The whole list floats into place. I would the list to be static, and just the rows move when necessary to reorder
List {
ForEach(players) { player in
Text(player.score)
}
}.animation(.default)
To animate only when something in the State data changes, use a withAnimation surrounding the part where you change it rather than the whole list:
import SwiftUI
struct Player: Identifiable {
var id = UUID()
var score: String
}
struct ContentView: View {
#State var players: [Player] = [
.init(score: "2"),
.init(score: "3"),
.init(score: "6"),
.init(score: "1")]
var body: some View {
VStack {
Button("shuffle") {
withAnimation(.easeIn) {
players.shuffle()
}
}
List {
ForEach(players) { player in
Text(player.score)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Related
I've just started learning swift and was going to build this number-incrementing sample app to understand MVVM. I don't understand why is my number on the view not updating upon clicking the button.
I tried to update the view everytime user clicks the button but the count stays at zero.
The View
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The ViewModel
import SwiftUI
class CounterViewModel: ObservableObject {
#ObservedObject var model = Model()
func increment() {
self.model.count += 1
}
}
The Model
import Foundation
class Model : ObservableObject{
#Published var count = 0
}
Following should work:
import SwiftUI
struct Model {
var count = 0
}
class CounterViewModel: ObservableObject {
#Published var model = Model()
func increment() {
self.model.count += 1
}
}
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Please note:
ObservableObject and #Published are designed to work together.
Only a value, that is in an observed object gets published and so the view updated.
A distinction between model and view model is not always necessary and the terms are somewhat misleading. You can just put the count var in the ViewModel. Like:
#Published var count = 1
It makes sense to have an own model struct (or class), when fx you fetch a record from a database or via a network request, than your Model would take the complete record.
Something like:
struct Adress {
let name: String
let street: String
let place: String
let email: String
}
Please also note the advantages (and disadvantages) of having immutable structs as a model. But this is another topic.
Hi it's a bad idea to use MVVM in SwiftUI because Swift is designed to take advantage of fast value types for view data like structs whereas MVVM uses slow objects for view data which leads to the kind of consistency bugs that SwiftUI's use of value types is designed to eliminate. It's a shame so many MVVM UIKit developers (and Harvard lecturers) have tried to push their MVVM garbage onto SwiftUI instead of learning it properly. Fortunately some of them are changing their ways.
When learning SwiftUI I believe it's best to learn value semantics first (where any value change to a struct is also a change to the struct itself), then the View struct (i.e. when body is called), then #Binding, then #State. e.g. have a play around with this:
// use a config struct like this for view data to group related vars
struct ContentViewConfig {
var count = 0 {
didSet {
// could do validation here, e.g. isValid = count < 10
}
}
// include other vars that are all related, e.g. you could have searchText and searchResults.
// use mutating func for logic that affects multiple vars
mutating func increment() {
count += 1
//othervar += 1
}
}
struct ContentView: View {
#State var config = ContentViewConfig() // normally structs are immutable, but #State makes it mutable like magic, so its like have a view model object right here, but better.
var body: some View {
VStack {
ContentView2(count: config.count)
ContentView3(config: $config)
}
}
}
// when designing a View first ask yourself what data does this need to do its job?
struct ContentView2: View {
let count: Int
// body is only called if count is different from the last time this was init.
var body: some View {
Text(count, format: .number)
}
}
struct ContentView3: View {
#Binding var config: ContentViewConfig
var body: some View {
Button(action: {
config.increment()
}) {
Text("Increment")
}
}
}
}
Then once you are comfortable with view data you can move on to model data which is when ObservableObject and singletons come into play, e.g.
struct Item: Identifiable {
let id = UUID()
var text = ""
}
class MyStore: ObservableObject {
#Published var items: [Item] = []
static var shared = MyStore()
static var preview = MyStore(preview: true)
init(preview: Bool = false) {
if preview {
items = [Item(text: "Test Item")]
}
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MyStore.shared)
}
}
}
struct ContentView: View {
#EnvironmentObject store: MyStore
var body: some View {
List($store.items) { $item in
TextField("Item", $item.text)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyStore.preview)
}
}
Note we use singletons because it would be dangerous to use #StateObject for model data because its lifetime is tied to something on screen we could accidentally lose all our model data which should have lifetime tied to the app running. Best to think of #StateObject when you need a reference type in a #State, i.e. involving view data.
When it comes to async networking use the new .task modifier and you can avoid #StateObject.
I have zipped my array of Strings and I don't know how to change the index of an array inside a zip Function in order to show the right image to the user.
Indeed, when the user chooses any row the same images are shown images[0].imagesString.
I want to show different images according to each row: When the row named super balloon is tapped, it shows an image of a balloon and superman, when sky good is tapped it shows an image of the sky and a thumb up tapped.
Do you have any suggestions?
Thank you so much :)
import SwiftUI
struct ImageInTheScreen:Identifiable {
var id = UUID()
let imagesString:[String]
let colorString: [Color]
let rowString:String
}
class ViewModel{
var images:[ImageInTheScreen] = [ImageInTheScreen(imagesString: ["super", "baloon"], colorString: [.red,.blue],rowString: " Super Baloon"),ImageInTheScreen(imagesString: ["sky","good"], colorString: [.red,.blue],rowString: "Sky good")]
var arrayForLoop : [(String,Color)] {
//when the user chooses any row the same images are shown images[0].imagesString.
let result =
zip(images[0].imagesString,images[0].colorString)
return result.map{($0.0,$0.1)}
}
}
struct ContentView: View {
let vm:ViewModel
var body: some View {
NavigationView{
List {
ForEach(vm.images) { sentence in
NavigationLink(
destination: QuestionView(),
label: {
Text(sentence.rowString)
})
}
}
}
}
}
struct QuestionView:View {
let vm = ViewModel()
var body: some View{
HStack{
ForEach(vm.arrayForLoop, id:\.0, content: {
image in
Image(image.0)
.resizable()
.frame(width: 140, height: 140)
.scaledToFit()
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(vm: ViewModel())
}
}
iOS 15 introduces the '.refreshable' View Modifier, but most of the examples I've seen use a List. I want to implement a pull to refresh feature in an app that does not use a list but just a VStack with Text views like the below. How can I implement this so that pull down to refresh will refresh my data?
import SwiftUI
struct ContentView: View {
#State var songName = "Song"
#State var artistName = "Artist"
var body: some View {
VStack {
Text(songName)
Text(artistName)
}
.refreshable {
reloadData()
}
}
private func reloadData() {
songName = "Let it Be"
artistName = "The Beatles"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm new to learning SwiftUI and XCode and am unable to figure out how to pass a variable from view to another. I read on #State and #Binding variables but from what I can tell that is for values that change. I have a static value that I calculate based on the date when the user opens the app.
The variable is the current moon phase and is stored locally in my main ContentView. I want to pass this variable to a second view that's accessed by clicking a NavigationLink.
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
let currentMoonPhaseArray = calculateMoonPhase()
let moonPhase = currentMoonPhaseArray[0]
NavigationView{
ScrollView(.vertical, showsIndicators:true) {
VStack(spacing:3){
NavigationLink(destination: MoonPhaseView()){
Text("Moon Phase - " + moonPhase)
}
}
}
.frame(maxWidth: .infinity)
.navigationTitle("MySky")
.navigationBarTitleDisplayMode(.inline)
}
}
}
MoonPhaseView.swift
import SwiftUI
struct MoonPhaseView: View {
var body: some View {
HStack{
Text("MoonPhaseView!")
}
}
}
struct MoonPhaseView_Previews: PreviewProvider {
static var previews: some View {
MoonPhaseView()
}
}
My goal is to have the calculated moon phase from ContentView.swift be passed to the MoonPhaseView.swift. I believe that bindings are the correct approach from what I've read, but all binding implementations seem to be for updating views often.
Thanks for any help or pointers!
You haven't shown what the type of moonPhase is, so I'm just going to use String as an example.
struct ContentView: View {
func calculateMoonPhase() -> [String] {
return ["Full","Waxing","Waning"]
}
var body: some View {
let currentMoonPhaseArray = calculateMoonPhase()
let moonPhase = currentMoonPhaseArray[0]
NavigationView{
ScrollView(.vertical, showsIndicators:true) {
VStack(spacing:3){
NavigationLink(destination: MoonPhaseView(phase: moonPhase)){
Text("Moon Phase - " + moonPhase)
}
}
}
.frame(maxWidth: .infinity)
.navigationTitle("MySky")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct MoonPhaseView: View {
var phase : String
var body: some View {
HStack{
Text("MoonPhaseView!")
Text(phase)
}
}
}
struct MoonPhaseView_Previews: PreviewProvider {
static var previews: some View {
MoonPhaseView(phase: "Full")
}
}
Note that every time MoonPhaseView is used, you must provide a phase parameter so that it has a value to fill var phase : String. You could provide a default value, but that doesn't seem like it would do much good here.
Not directly related to your question, but I might suggest that calculating the phase in the body might lead to undesirable results, especially if it's an expensive calculation or it has to contact an API or something. You might want to consider doing this in onAppear and keeping it in a #State variable in your ContentView, or perhaps even using an ObservableObject as a view model and storing the phase there.
You can use "Environment" to pass system-wide settings to views and child views.
For example:
#main
struct TestApp: App {
let moonPhaseValue = "Waxing" // <--- some value
var body: some Scene {
WindowGroup {
ContentView().environment(\.moonPhase, moonPhaseValue) // <--- pass it around
}
}
}
struct ContentView: View {
#Environment(\.moonPhase) var moonPhase // <--- the value again
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: MoonPhaseView()) {
Text("Moon Phase - " + moonPhase)
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct MoonPhaseView: View {
#Environment(\.moonPhase) var moonPhase // <--- the value again
var body: some View {
HStack{
Text("MoonPhaseView is \(moonPhase)")
}
}
}
// create your own EnvironmentKey
struct MoonPhaseKey: EnvironmentKey {
static let defaultValue: String = "Full"
}
// create your own EnvironmentValues
extension EnvironmentValues {
var moonPhase: String {
get { return self[MoonPhaseKey] }
set { self[MoonPhaseKey] = newValue }
}
}
I have a SwiftUI text label and i want to write something in it after I press a button.
Here is my code:
Button(action: {
registerRequest() // here is where the variable message changes its value
}) {
Text("SignUp")
}
Text(message) // this is the label that I want to change
How do I do this?
With only the code you shared it is hard to say exactly how you should do it but here are a couple good ways:
The first way is to put the string in a #State variable so that it can be mutated and any change to it will cause an update to the view. Here is an example that you can test with Live Previews:
import SwiftUI
struct UpdateTextView: View {
#State var textToUpdate = "Update me!"
var body: some View {
VStack {
Button(action: {
self.textToUpdate = "I've been udpated!"
}) {
Text("SignUp")
}
Text(textToUpdate)
}
}
}
struct UpdateTextView_Previews: PreviewProvider {
static var previews: some View {
UpdateTextView()
}
}
If your string is stored in a class that is external to the view you can use implement the ObservableObject protocol on your class and make the string variable #Published so that any change to it will cause an update to the view. In the view you need to make your class variable an #ObservedObject to finish hooking it all up. Here is an example you can play with in Live Previews:
import SwiftUI
class ExternalModel: ObservableObject {
#Published var textToUpdate: String = "Update me!"
func registerRequest() {
// other functionality
textToUpdate = "I've been updated!"
}
}
struct UpdateTextViewExternal: View {
#ObservedObject var viewModel: ExternalModel
var body: some View {
VStack {
Button(action: {
self.viewModel.registerRequest()
}) {
Text("SignUp")
}
Text(self.viewModel.textToUpdate)
}
}
}
struct UpdateTextViewExternal_Previews: PreviewProvider {
static var previews: some View {
UpdateTextViewExternal(viewModel: ExternalModel())
}
}