BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

SOLVED: Swift Functions In Swift UI

Forums > SwiftUI

I love the site and find it incredibly useful in trying to learn Swift. I am trying to learn Swift ( I am VERY new to learning Swift) by making a very basic barbell weight plate calculator app.

Goal: The user enters a weight value and the app displays the weight plates needed per side of the bar.

I wrote a function in playgrounds that does what I expect it to do to tell me how many plates on one side of the bar I would need given a certain goal value.

My issue is that I don't understand how to implement this function in SwiftUI in my app so I can enter the value in a text field and get the function to run.

Here is my playground code:

import UIKit

func barbellweight (weight: Int){
    var plate_hash : [Int:Int] = [:]

    if weight == 45 {
        print("You only need the bar!")
    }else if weight < 45{
        print("Must be divisible by 5!")
    }else if (weight % 5 != 0){
           print("Must be divisible by 5!")
    }else{

        let plate_array = [45, 35, 25, 10, 5, 2.5]
        var one_side_weight = Double(weight - 45) / 2.0

        for plate_size in plate_array {
            var plate_amount = (one_side_weight / plate_size)
            plate_amount.round(.towardZero)
            one_side_weight -= (plate_size * plate_amount)
            plate_hash[Int(plate_size)] = Int(plate_amount)
        }
    }
    let plate_hash_filtered = plate_hash.filter { $0.value > 0 }
    //print(plate_hash_filtered)
    print(plate_hash_filtered)
}

barbellweight(weight: 225)

Here is attempt to implement it in Swift UI but without any luck. I know it's deconstructed and slightly different - I don't quite understand how to integrate a function into SwiftUI. If someone has any recommendations for resources to look at for this specific ask I would really appreciate it.

import SwiftUI

struct Weight_Plate: View {
    @State var weight: String = "135"
    @State var plate_hash = [String]()
    @State var plate_array = [45, 35, 25, 10, 5, 2.5]

    var body: some View {
        var one_side_weight = Double(Int(weight)! - 45) / 2.0

        List{
            Text("Number of Plates Needed Per Side")
                .multilineTextAlignment(.center)
            ForEach(self.plate_array, id: \.self) { plate_size in
                var plate_amount = (one_side_weight / plate_size)
                if Int(weight) == 45 {
                    Text("You only need the bar!")
                } else if Int(weight)! < 45 {
                    Text("Must be divisible by 5!")
                } else if (Int(weight)! % 5 != 0) {
                       Text("Must be divisible by 5!")
                } else {
                        //Text("Error")
                        plate_amount.round(.towardZero)
                        one_side_weight -= (plate_size * plate_amount)
                    Text("\(Int(plate_size)) x \(Int(plate_amount))")

                       // Text("\(plate):\(Int(plate_amount))")
            }
        }

        HStack(alignment: .center) {
            Text("Weight:")
                .font(.callout)
                .bold()
            TextField("Enter Desired Weight", text: $weight)
                .textFieldStyle(RoundedBorderTextFieldStyle())
        }.padding()
    }
}

}
struct Weight_Plate_Previews: PreviewProvider {
    static var previews: some View {
        Weight_Plate()
    }
}

As a note, I also posted this question on Stack Overflow here: https://stackoverflow.com/questions/65973652/function-in-swift

I really appreciate any guidance and/or help with this!

1      

So, that big if/else in the middle of your ForEach should actually be a separate function. I don't have Xcode open at this second, but here's what you really want:

struct Weight_Plate: View {
// ... all the stuff you wrote already, just fine...
...
    var body: some View {
        List{
            Text("Number of Plates Needed Per Side")
                .multilineTextAlignment(.center)
            ForEach(self.plate_array, id: \.self) { plate_size in
                getPlatesText(plate_size)
            }
            ...
        }
    }

    func getPlatesText(_ plateSize: Int) -> Text {
        let one_side_weight = Double(Int(weight)! - 45) / 2.0
        var plate_amount = (one_side_weight / plateSize)
        if Int(weight) == 45 {
            return Text("You only need the bar!")
        } else if Int(weight)! < 45 {
             return Text("Must be divisible by 5!")
        } else if (Int(weight)! % 5 != 0) {
             return Text("Must be divisible by 5!")
         } else {
             plate_amount.round(.towardZero)
             return Text("\(Int(plateSize)) x \(Int(plate_amount))")
         }
     }

The point being, the logic sits out in the function getPlatesText and that function always returns a Text, so this makes your body a little easier to look at and process, as it is simply declarative, "put this view, then this other view, and stack some views..."

1      

I agree with. @sbeitzel you have alot going on in the body. I had a play around with it and come up with this

struct ContentView: View {
    @State private var desiredWeight = ""
    @State private var calculatedPlates: [Double] = []
    @State private var message = ""
    @State private var showingAlert = false

    var body: some View {
        VStack {
            TextField("Enter Desired Weight", text: $desiredWeight)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()

            Button("Calculate Weights", action: calculatePlates)

            List {
                ForEach(calculatedPlates, id: \.self) { calculatedPlate in
                    Text("\(calculatedPlate, specifier: "%.1f")")
                }
            }
        }
        .alert(isPresented: $showingAlert) {
            Alert(title: Text(message), dismissButton: .default(Text("OK")))
        }
    }

    func calculatePlates() {
        calculatedPlates = []
        let plates = [45, 35, 25, 10, 5, 2.5]

        if let weight = Double(desiredWeight) { // Check that it a Double
            if weight == 45 {
                message = "You only need the bar!"
                showingAlert = true
            } else if weight < 45 {
                message = "Must be above bar weight of 45"
                showingAlert = true
            } else if (Int(weight) % 5) != 0 {
                message = "Must be divisible by 5"
                showingAlert = true
            } else {
                var oneSideWeight = (weight - 45) / 2

                for plate in plates {
                    while (oneSideWeight - plate) >= 0 {
                        oneSideWeight -= plate
                        calculatedPlates.append(plate)
                    }
                }
            }
        }else {
            message = "Enter a valid number"
            showingAlert = true
            return
        }
    }
}

also safely unwraps the input amount

3      

Well this being a learning forum, and you already 2 great answers, here's some additional info that I hope helps:

1- Naming:

While it certainly is good to have consistency in the way we write code, it is also important to adopt "best practices" for each language. Accordingly, I'm not sure what plate_hash is for (in SwiftUI), but plate_array is better named plateWeights or plates as per the comments.

Also, you can definitely use the underscore in naming, but the convention with Swift is to use camelCase. As you start reading code, you will notice the overwhelming majority use camelCase over underscores.

And your weight property is better defined as per @NigelGee's example: desiredWeight which makes it much clearer.

In reference to your playground example, your function barbellWeight(weight: Int) I would like to highlight @sbeitzel comment where he uses the underscore before the parameter. This allows you to skip the writing out the parameter name when you call the function. So you would be better off writing it as barbellWeight(_ weight: Int)

2- Conditional Statements:

This is more of a personal choice, but I figured I might as well share. Rather than going with the multiple if/else statements I prefer a couple well placed guard statements, which will help clarify that there is no need for us to continue if these conditions are not met. I will use @NigelGee's function

    func calculatePlates() {
        calculatedPlates = []
        let plates = [45, 35, 25, 10, 5, 2.5]

        // Check that it is a Double (or numeric)
        guard let weight = Double(desiredWeight) else {
            message = "Enter a valid number"
            showingAlert = true
            return
        }

        // Check that it is higher than 45 otherwise just use the bar
        guard weight > 45 else {
        // Change message to include conditions
            message = "Must be above bar weight of 45" 
            showingAlert = true
            return
        }

        // Check that it is a multiple of 5 otherwise we don't have the relevant plates
        guard Int(weight) % 5 == 0 else {
            message = "Must be divisible by 5"
            showingAlert = true
            return
        }

        // here you calculate the plates needed
        // we're all clear.
    }

This, at least for me, is much clearer. It is clearly stating that these conditions must be met, otherwise we have an error.

3- Functions in SwiftUI:

The thing about SwiftUI that I believe will help you answer your question is this: "SwiftUI needs to be able to render views which we describe. @State properties, when they change, will cause SwiftUI to re-render the views."

a TextField in SwiftUI needs to be passed a binding. As the binding changes, SwiftUI renders all the relevant parts. It is obviously more complex than this, but that would be beyond my grasp at this stage.

Therefore, if you want a function to run, when the TextField changes, then you need the function to be called at the relevant place and be connected to data that changes along with any state property. You can do this via a computed property, or by following one of the 2 other comments.

So if you start with some function, and want to translate that into SwiftUI (or UIKit for that matter), focus on the Goal. Not the function.

4- It's all about the Goal:

Whether you use functions or computed properties, or anything else really, is up to you. As long as they lead you towards the goal.

In your case, the app needs to select an overall weight, at which point the app will recommend how many plates on each side are needed.

Since you are restricting the user to your plate_array you might as well, use these with a Picker:

            Picker("Desired Weight", selection: $weight) {
                ForEach(plate_array, id: \.self) { plate in
                    Text("\((plate * 2) + 45, specifier: "%g")")
                }
            }
            .pickerStyle(SegmentedPickerStyle())

This way, you can avoid having to check whether the user made any kind of mistake. Also, you can add the 0 option in your array, for just the bar.

If you prefer the TextField option, then you can use .keyboardType(.decimalPad) modifier to minimize the possibility of text.

Conclusion (TL;DR):

SwiftUI renders the views each time a state property changes, but best to consider that the changes only occur where necessary instead of a complete re-render of everything on screen. TextFields take a binding, which changes state, therefore, any part that is connected to this state, will change as well.

Understanding your data flow will greatly help you with SwiftUI. Think of the data you want, where you want it, and write things accordingly. Avoid lots of code in your body property, and extract into functions or smaller views, whenever possible.

Playgrounds are a great way to experiment and try out the business logic. Adding the UI layer should not change that. Keep the logic separate, and the only thing left is the UI elements which can be simple or complex.

3      

Thank you, @sbeitzel, @NigelGee and @MarcusKay for all the help with and thoughts on this! I greatly appreciate it.

I am going to work through all of these examples and find what works best for this project. Again, I really appreciate the help as I learn Swift!

1      

Some great points by @MarcusKay and in my defence I did this at 1.30 in morning!

First I would of added .keyboardType(.numberPad) not the .decimalPad as you need the number divisable by 5 but forgot it as I was trying to get the logic right.

Second I was orginal going with guard let as @MarcusKay suggested and to be fair would of done it that way myself, however wanted to keep some flow with the orginal code is why choose if let and if/else

PS you need to add one more guard let for the bar only statement.

With regard to Picker, it was my understanding that a person can enter any desired weight not just those in the array!

As with the snake_case and camelCase, you can use either but as @MarcusKay said it general in Swift that camelCase is used in most places and therefore easy for people to read your code, but of the complier do not care!!!

But always try to keep as much logic out of the body does seem to the best practice either with computed properties and functions

Happy coding

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.