UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Changing @State to @Binding

Forums > SwiftUI

@Jonne  

Hi,

For my practice I'm working a small project and have a @State variable that needs to be a @Binding var. When I change the variable I get some errors that I don't know how to fix. I searched and tried some different things, but with no luck. Can anyone point me in the right direction?

@State variable

var body: some View {

        @State var dateToDisplay: Date = Calendar.current.date(byAdding: .day, value: snappedDayOffset, to: baseDate) ?? baseDate

        ....

Complete view

import SwiftUI

struct NavView: View {
    let nPanels = 15
    let panelSize: CGFloat = 50
    let gapSize: CGFloat = 10
    let baseDate = Date.now
    let nameOfTheDayOfMonthFormatter = DateFormatter()
    let dayOfMonthFormatter = DateFormatter()
    let monthNameFormatter = DateFormatter()

    @State private var snappedDayOffset = 0
    @State private var draggedDayOffset = Double.zero

    init() {
        nameOfTheDayOfMonthFormatter.dateFormat = "EEEE"
        dayOfMonthFormatter.dateFormat = "dd"
        monthNameFormatter.dateFormat = "MMMM"
    }

    private var positionWidth: CGFloat {
        CGFloat(panelSize + gapSize)
    }

    private func xOffsetForIndex(index: Int) -> Double {
        let midIndex = Double(nPanels / 2)
        var dIndex = (Double(index) - draggedDayOffset - midIndex).truncatingRemainder(dividingBy: Double(nPanels))
        if dIndex < -midIndex {
            dIndex += Double(nPanels)
        } else if dIndex > midIndex {
            dIndex -= Double(nPanels)
        }
        return dIndex * positionWidth
    }

    private func dayAdjustmentForIndex(index: Int) -> Int {
        let midIndex = nPanels / 2
        var dIndex = (index - snappedDayOffset - midIndex) % nPanels
        if dIndex < -midIndex {
            dIndex += nPanels
        } else if dIndex > midIndex {
            dIndex -= nPanels
        }
        return dIndex + snappedDayOffset
    }

    private func dateView(index: Int, halfFullWidth: CGFloat) -> some View {
        let xOffset = xOffsetForIndex(index: index)
        let dayAdjustment = dayAdjustmentForIndex(index: index)
        let dateToDisplayLocal = Calendar.current.date(byAdding: .day, value: dayAdjustment, to: baseDate) ?? baseDate
        return ZStack {
            Color.blue
                .clipShape(Circle())
            VStack {
                let nameOfTheDay = nameOfTheDayOfMonthFormatter.string(from: dateToDisplayLocal)
                Text(nameOfTheDay.prefix(1))
            }
            .foregroundStyle(.white)
            .fontWeight(dayAdjustment == 0 ? .bold : .regular)
        }
        .frame(width: panelSize, height: panelSize)
        .offset(x: xOffset)

        .opacity(xOffset + positionWidth < -halfFullWidth || xOffset - positionWidth > halfFullWidth ? 0 : 1)
    }

    private var dragged: some Gesture {
        DragGesture()
            .onChanged() { val in
                draggedDayOffset = Double(snappedDayOffset) - (val.translation.width / positionWidth)
            }
            .onEnded { val in
                snappedDayOffset = Int(Double(snappedDayOffset) - (val.predictedEndTranslation.width / positionWidth).rounded())
                withAnimation(.easeInOut(duration: 0.15)) {
                    draggedDayOffset = Double(snappedDayOffset)

                }
            }
    }

    var body: some View {

        @State var dateToDisplay: Date = Calendar.current.date(byAdding: .day, value: snappedDayOffset, to: baseDate) ?? baseDate

        GeometryReader { proxy in
            let halfFullWidth = proxy.size.width / 2
            //

            VStack() {
                HStack {

                     if snappedDayOffset == 0 {
                        Text("Today")
                    } else if snappedDayOffset == -1 {
                        Text("Yesterday")
                    } else if snappedDayOffset == 1 {
                        Text("Tomorrow")
                    } else {
                        Text(nameOfTheDayOfMonthFormatter.string(from: dateToDisplay))
                    }
                    Text(dayOfMonthFormatter.string(from: dateToDisplay))
                    Text(monthNameFormatter.string(from: dateToDisplay))
                }
                .font(.title)
                .bold()
                .padding()

                VStack{
                    Color.black
                    .frame(height: 2 / UIScreen.main.scale)
                }
                Image(systemName: "arrowtriangle.down.fill")
                    .offset(y:-2)

                ZStack {
                    ForEach(0..<nPanels, id: \.self) { index in
                        dateView(index: index, halfFullWidth: halfFullWidth)
                    }
                }
                .gesture(dragged)
                .frame(maxWidth: .infinity)

                VStack {
                    Text(dateToDisplay.description)
                }
                .padding()
            }

        }
        .frame(maxHeight: 200)
    }
}

#Preview {
    NavView()
}

   

Computed Properties

What is a computed property?

You may design your application with variables that change based on user's input. For example, what's the difficulty setting for a game your user is about to play? How many levels in the game? Is the game over yet?

Other variables aren't based on a user's choice, instead they are calculated. For example:

var currentLevel: Int    // this is the game level the user is trying to complete
let gameOverLevel = 10   // this is how many levels the user must complete

// This is a variable named isGameOver. It is a boolean. 
// It is computed based on user's current level and the gameOverLevel
var isGameOver: Bool {
    currentLevel > gameOverLevel  // <--- it's computed!
}

body is a Computed Property

Now look closely at your NavView struct. The view's body is a COMPUTED VARIABLE.

It takes a box of parts, perhaps some variables and it has a calculation with ONE purpose. That is, at the end of its calculations, it will produce a view. It produces some view that SwiftUI is able to turn into pixels and push to the screen.

The body var might need a date to display. You may call this variable dateToDisplay. You may want to keep this variable around for a few hundred view cycles. To do this, you mark the variable with an @State property wrapper.

However, you must do this OUTSIDE the body's calculation.

This is a concept taught in the earliest lessons in the 100 Days of SwiftUI. Are you following the 100 Days of SwiftUI course? or are you guessing at SwiftUI syntax?

Keep Coding

Maybe this lesson will help?

See -> Custom Component

1      

This is not taught in early 100 Days of SwiftUI lessons.

Why do you have vertical stacks with only one item in them?

// This does not make sense to me.
VStack {
    Text(dateToDisplay.description)
}

// Nor does this
VStack {
    Color.black
         .frame(height: 2 / UIScreen.main.scale)
}

1      

@Jonne  

@Obelix, Thank you for your reply. Your answer makes sense, but when I place it outside the bodies view I have other problems, so I started trying things. I will look into your feedback and how I can fix this. As for your questions:

I am working on the 100 days of swiftui, currently at day 72. But I noticed I was starting to forget the things I've learned so I started to work on something of my own to implement and refresh the knowledge.

The first VStack with one line is just a remainder of some testing I did. It's temporary and will be removed.

The second VStack is just a temporarly horizontal line

   

Now we're getting somewhere...

@Obelix, Thank you for your reply. Your answer makes sense, but when I place it outside the bodies view I have other problems, so I started trying things.

Keep Coding!

Show us your revised code and show us your errors. More imporantly! Tell us what you're attempting to achieve and let us know what you think your code is doing! We can give you tips, hints, and additional lessons. Some of us will give you the direct answer! (Personally, I prefer you try and learn. So i seldom give a direct answer! Sorry!!)

1      

@Jonne  

@Obelix,

I'm having trouble fixing my issue. I moved the @State var outside the body (to the property initializer?) and changed it to a @Binding var. Now I get the error "Cannot use instance member 'baseDate' within property initializer" and "Cannot convert value of type 'Date' to specified type 'Binding<Date>'". I tried to make the binding a Property wrapper, but now I get the error "Property wrapper cannot be applied to a computed property" which tells me I cannot do this with a @Binding. I also tried to move the content of the var in a method buat that's also no allowed.

The reason I want "dateToDisplay" to be a binding is because I place this view (NavView) inside another view, and there I want to filter an array with the value from "dateToDisplay". Inside the NavView you can select another date with a slider that changes the result from the array. Hope this makes sense :-)

I'm not looking for the straight up answer, I want to learn, but I'm having trouble to find the right direction to search for the solution.

Hope you can help me out again, thanks in advance.

   

@Jonne  

I solved the issue.

import SwiftUI

struct NavView: View {
    let nPanels = 15
    let panelSize: CGFloat = 50
    let gapSize: CGFloat = 10
    let baseDate = Date.now
    let nameOfTheDayOfMonthFormatter = DateFormatter()
    let dayOfMonthFormatter = DateFormatter()
    let monthNameFormatter = DateFormatter()

    @State private var snappedDayOffset = 0
    @State private var draggedDayOffset = Double.zero

    @Binding var dateToDisplay: Date

    private var positionWidth: CGFloat {
        CGFloat(panelSize + gapSize)
    }

    private func xOffsetForIndex(index: Int) -> Double {
        let midIndex = Double(nPanels / 2)
        var dIndex = (Double(index) - draggedDayOffset - midIndex).truncatingRemainder(dividingBy: Double(nPanels))
        if dIndex < -midIndex {
            dIndex += Double(nPanels)
        } else if dIndex > midIndex {
            dIndex -= Double(nPanels)
        }
        return dIndex * positionWidth
    }

    private func dayAdjustmentForIndex(index: Int) -> Int {
        let midIndex = nPanels / 2
        var dIndex = (index - snappedDayOffset - midIndex) % nPanels
        if dIndex < -midIndex {
            dIndex += nPanels
        } else if dIndex > midIndex {
            dIndex -= nPanels
        }
        return dIndex + snappedDayOffset
    }

    private func dateView(index: Int, halfFullWidth: CGFloat) -> some View {
        let xOffset = xOffsetForIndex(index: index)
        let dayAdjustment = dayAdjustmentForIndex(index: index)
        let dateToDisplayLocal = Calendar.current.date(byAdding: .day, value: dayAdjustment, to: baseDate) ?? baseDate
        return ZStack {
            Color.blue
                .clipShape(/*@START_MENU_TOKEN@*/Circle()/*@END_MENU_TOKEN@*/)
            VStack {
                let nameOfTheDay = nameOfTheDayOfMonthFormatter.string(from: dateToDisplayLocal)
                Text(nameOfTheDay.prefix(1))
            }
            .foregroundStyle(.white)
            .fontWeight(dayAdjustment == 0 ? .bold : .regular)
        }
        .frame(width: panelSize, height: panelSize)
        .offset(x: xOffset)

        // Setting opacity helps to avoid blinks when switching sides
        .opacity(xOffset + positionWidth < -halfFullWidth || xOffset - positionWidth > halfFullWidth ? 0 : 1)
    }

    private var dragged: some Gesture {
        DragGesture()
            .onChanged() { val in
                draggedDayOffset = Double(snappedDayOffset) - (val.translation.width / positionWidth)
            }
            .onEnded { val in
                snappedDayOffset = Int(Double(snappedDayOffset) - (val.predictedEndTranslation.width / positionWidth).rounded())
                withAnimation(.easeInOut(duration: 0.15)) {
                    draggedDayOffset = Double(snappedDayOffset)
                }
                dateToDisplay = Calendar.current.date(byAdding: .day, value: snappedDayOffset, to: baseDate) ?? baseDate
                print(snappedDayOffset)
            }
    }
    init(dateToDisplay: Binding<Date>) {
        nameOfTheDayOfMonthFormatter.dateFormat = "EEEE"
        dayOfMonthFormatter.dateFormat = "dd"
        monthNameFormatter.dateFormat = "MMMM"
        _dateToDisplay = dateToDisplay
    }

    var body: some View {

    ...

The value now gets updated on drag end. Good enough for now.

   

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Click to save your free spot now

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

Reply to this topic…

You need to create an account or log in to reply.

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.