Very odd tuple restructure of two elements: both get the string value of the first element triggered by just adding a let property

I can't figure why my code does this:

struct CounterView: View {
    // both are ":"!
    let (timeSeparator, clockDisplayTemplate) = {
print("HERE HERE")      // this is printed twice in console
        return (":", "88:88:88")
    }()

    // ....
}

HERE HERE is printed twice in the console and timeSeparator, clockDisplayTemplate both are ";". This works correctly if I just extract my code out to a playground. So this oddity reproduce only with my code specifically but I just could figure why, and why does the closure run twice?

My complete code
import SwiftUI
import ComposableArchitecture

 ...

struct CounterView: View {
    
    
    let store: StoreOf<CounterFeature>
    
    let accentColor = Color.pink
    let counterFont = Font.custom("Digital-7Monoitalic", fixedSize: 45).monospacedDigit()
    let clockFont = Font.custom("Digital-7Monoitalic", fixedSize: 215).monospacedDigit()
        
    let (timeSeparator, clockDisplayTemplate) = {
print("HERE HERE")      // this is printed twice in console
        return (":", "88:88:88")
    }()



    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { store in
            VStack(spacing: 5) {
                Text("timeSeparator: \(timeSeparator)")     // ":"
                Text("clockDisplayTemplate: \(clockDisplayTemplate)")   // this is also ":"

                TimelineView(.periodic(from: .now, by: 0.2)) { context in
                    clockDisplay(date: context.date)
                }

                HStack(spacing: 15) {
                    Button {
                        store.send(.decrementButtonTapped, animation: .spring)
                    } label: {
                        Image(systemName: "minus")
                            .font(counterFont)
                    }
                    .buttonStyle(.symbolButtonStyle)
                    .buttonRepeatBehavior(.enabled)
                    .sensoryFeedback(.decrease, trigger: store.count)
                    
                    counterDisplay(store: store)
                    
                    Button {
                        store.send(.incrementButtonTapped, animation: .spring)
                    } label: {
                        Image(systemName: "plus")
                            .font(counterFont)
                    }
                    .buttonStyle(.symbolButtonStyle)
                    .buttonRepeatBehavior(.enabled)
                    .sensoryFeedback(.increase, trigger: store.count)
                }
                .padding(.horizontal)
            }
        }
    }
    
    func clockDisplay(date: Date) -> some View {
        Text(clockDisplayTemplate)
            .font(clockFont)
            .lineLimit(1)
            .padding(.horizontal, 20)
            .minimumScaleFactor(0.2)
            .foregroundColor(.gray)
            .opacity(0.25)
            .overlay {
                // this does 24-hour style. Who figured this out?
                // https://forums.swift.org/t/new-date-formatstyle-anyway-to-do-24-hour/52994/34
                Text(date,
                     format: Date.VerbatimFormatStyle(format: "\(hour: .twoDigits(clock: .twentyFourHour, hourCycle: .oneBased)):\(minute: .twoDigits):\(second: .twoDigits)",
                     timeZone: .current,
                     calendar: .current)
                )
                .font(clockFont)
                .lineLimit(1)
                .padding(.horizontal, 20)
                .minimumScaleFactor(0.2)
                .foregroundColor(accentColor)
                .contentTransition(.numericText(value: date.timeIntervalSince1970.rounded()))
                .animation(.spring, value: date.timeIntervalSince1970)
            }
    }
    
    func counterDisplay(store: ViewStore<CounterFeature.State, CounterFeature.Action>) -> some View {
        // Use this view for sizing
        Text(90_000.formatted().dropFirst())
            .font(counterFont)
            .lineLimit(1)
            .foregroundColor(.gray)
            .opacity(0.25)
        
            .overlay {
                Text(store.count, format: .number)
                    .font(counterFont)
                    .foregroundColor(accentColor)
                    .frame(maxWidth: .infinity, alignment: .trailing)
                    .contentTransition(.numericText(value: Double(store.count)))
            }
            .padding(.init(top: 15, leading: 20, bottom: 15, trailing: 20))
            .background {
                RoundedRectangle(cornerRadius: 20, style: .continuous)
                    .strokeBorder(accentColor, lineWidth: 2)
            }

    }
}

#Preview {
    CounterView(
      store: Store(initialState: CounterFeature.State()) {
        CounterFeature()
      }
    )
}

Why both values are :, I don't know, but with regards to the double printing, I don't think you can rely on your view struct being initialized only once, or a certain number of times. I think SwiftUI is free to reevaluate the view hierarchy description whenever it pleases, and that includes both initialising the view structs, as well as evaluating the body property of those views.

2 Likes

It's fine if the CounterView is created twice.

I just cannot figure out why clockDisplayTemplate is ":" and not "88:88:88"

if I make it a static let, it worked as expected.

why is this so? Since if I remember correctly, static let are lazy. Maybe this is why it work?

But non-static let should work:

import SwiftUI

struct FooView: View {
    let (timeSeparator, clockDisplayTemplate) = {
print("HERE HERE")      // this is printed twice in console
        let result = (":", "88:88:88")
print("    result: \(result)")
        return result
    }()

    
    var body: some View {
        VStack {
            Text("timeSeparator: \(timeSeparator)")
            Text("clockDisplayTemplate: \(clockDisplayTemplate)")
        }
    }
}

#Preview {
    FooView()
}

This works as expected.

Why is let fail, static let works?

Could you make a minimal example that still doesn't work for you? I tried your last fragment - it works fine for me.

:scream: Just adding in the ComposableArchitecture cause the problem to occur:

In a new iOS app Xcode project, add package The Composable Architecture

Add a new FooBarView and copy paste this:

import SwiftUI
import ComposableArchitecture


struct FooBarView: View {
    
    // just adding this cause the problem
    let store: StoreOf<CounterFeature>
    
    // doing it this way is no problem
//    let store: StoreOf<CounterFeature> = Store(initialState: CounterFeature.State()) {
//        CounterFeature()
//    }
    
    // adding in TCA, problem here: both get ":"
    let (timeSeparator, clockDisplayTemplate) = (":", "88:88:88")
    
    static let (aaa, bbb) = (":", "88:88:88")

    var body: some View {
        VStack {
            Text(Date.now, format: .dateTime.hour().minute().second())
            Color.clear.frame(height: 30)
            Text("timeSeparator \"\(timeSeparator)\"")
            Text("clockDisplayTemplate \"\(clockDisplayTemplate)\"")
            
            Divider()
            
            Text("aaa \"\(Self.aaa)\"")
            Text("bbb \"\(Self.bbb)\"")

        }
        .padding()
    }
}







struct CounterFeature: Reducer {
    struct State: Equatable {
        var count = 99
        var numberFactAlert: String?
    }
    
    enum Action: Equatable {
      case decrementButtonTapped
      case incrementButtonTapped
    }

    
    func reduce(into state: inout State, action: Action) -> Effect<Action> {
      switch action {
      case .decrementButtonTapped:
        state.count -= 1
        if state.count < 0 {
            state.count = 9_999
        }
        return .none

      case .incrementButtonTapped:
        state.count += 1
        if state.count > 9_999 {
            state.count = 0
        }
        return .none
      }
    }
}


#Preview {
    
//    FooBarView()
    
    
    
    FooBarView(
      store: Store(initialState: CounterFeature.State()) {
        CounterFeature()
      }
    )

    
}

So how can this be?

1 Like

Hi @young, this has nothing to do with the Composable Architecture. It is reproducible by just holding onto any value in the view:

struct FooBarView: View {
  let value: Int

  let (timeSeparator, clockDisplayTemplate) = (":", "88:88:88")
  static let (aaa, bbb) = (":", "88:88:88")
  …
}

No idea why this is happening, but it does not have to do with our library.

1 Like

I see. Thank you!

struct FooBarView: View {
  // what's the difference between this which cause this problem
  let value: Int
  // add this and problem goes away!
  let xxx = 1   // assigning here doesn't cause this problem

  let (timeSeparator, clockDisplayTemplate) = (":", "88:88:88")
  static let (aaa, bbb) = (":", "88:88:88")
  …
}
Test
import SwiftUI

struct FooView: View {
    let value = 1
    let xxx = 1
    let (a, b) = (":", "88:88:88")
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
            
            Text("a = \(a)")
            Text("b = \(b)")
        }
        .padding()
    }
}

#Preview {
    FooView()
//    FooView(value: 1)
}

This one works fine for me as well. I am on Xcode 14.3. Could be a bug in a more recent Xcode version?

I'm on Xcode Version 15.0 (15A240d)

I hope this is a bug that can be fix.

This looks like a bug in Xcode Version 15.0 (15A240d). I can even make the compiler fail with nonzero exit:

import SwiftUI

struct ContentView: View {
    // what's the difference between this which cause this problem
    let value: Int
    // if doing it this way, no problem
//    let value = 1
    
    // all these tuple destructure the 2nd and subsequent elements are wrong!
    let (timeSeparator, clockDisplayTemplate) = (":", "88:88:88")
    let (aaa, bbb) = (":", "88:88:88")
    let (xxx, yyy) = ("one", 1)
    
    // uncomment this, compiler fail with nonzero exit!!!!
//    let (z1, z2, z3) = ("one", 1, Double.pi)

    var body: some View {
        VStack {
            Text("timeSeparator = \(timeSeparator)")
            Text("clockDisplayTemplate = \(clockDisplayTemplate)")  // !! wrong
            Divider()
            Text("aaa = \(aaa)")
            Text("bbb = \(bbb)")        // !! Wrong
            Text("xxx = \(xxx)")
            Text("yyy = \(yyy)")        // Wrong
            
//            Text("z1 = \(z1)")
//            Text("z2 = \(z2)")
//            Text("z3 = \(z3)")
        }
        .padding()
    }
}

#Preview {
    ContentView(value: 123)
}

How to report this bug? Xcode, Swift compiler?

I can even make the compiler fail with nonzero exit

Well, that’s not good.

How to report this bug? Xcode, Swift compiler?

My standard advice is:

Either way, please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

I can reproduce with minimal command line code:

struct Foo {
    let value: Int

    let (aaa, bbb) = ("aaa", "bbb")     // bbb is wrong! it's "aaa" but should be "bbb"

        // uncomment this to make the compiler fail with non-zero exit
//    let (z1, z2, z3) = ("one", 1, Double.pi)


    func tellMe() {
        print(foo.aaa)
        print(foo.bbb)          // output:  aaa


        assert(aaa == "aaa")
        assert(bbb == "bbb", "bbb should be bbb but it's \(bbb)")
    }

}


let foo = Foo(value: 1)


foo.tellMe()




print("Hello")

I'm not sure if incorrect tuple destructure and the compiler non-zero exit are related so I filed two bugs:

FB13224203 compiler non-zero exit

FB13224237 incorrect tuple destructure

Bug report doesn't ask which version of Xcode. I'm on Xcode Version 15.0 (15A240d)

Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)

I think @eskimo's advice, then, would be to file this against Swift rather than Xcode. Indeed, I can minimize your example further:

struct Foo {
    let value: Int
    let (aaa, bbb) = ("aaa", "bbb")
    // let (z1, z2, z3) = ("one", 1, Double.pi)
}

let foo = Foo(value: 1)
print(foo.bbb) // "aaa"

This didn't compile at all before Swift 5.6, and has given the unexpected value ever since then. Which is not great...

4 Likes

Was it ever working for you? Testing on godbolt on various swift versions it either results either "Foo cannot be constructed" or a compiler crash or incorrect app (your assert fires).

I report the bugs by following the url in the error message. I can choose the Swift category.

Please submit a bug report (https://swift.org/contributing/#reporting-bugs) and include the crash backtrace.

Should I file these bugs on GitHub?

Compiler non-zero exit
Incorrect tuple destructure

Thanks for bringing this bug to our attention! I looked through the affected code's history, and it seems to confirm what Xiaodi said, that this particular case of a destructuring stored let property binding in a struct has never been implemented correctly. I'm trying out a patch to at least raise a compiler error so people don't run into the miscompile: [wip] disable let stored property destructuring by jckarter · Pull Request #68930 · apple/swift · GitHub

6 Likes

@Joe_Groff Question (probably better posted on the PR, but I'm not logged into GitHub on this device): does this destructuring bug really only affect structs?

1 Like

The bug is in the code generation for the implicit elementwise initializer, which is only generated for structs.

3 Likes

It works fine for static let (aaa, bbb) = ("aaa", "bbb")

Static properties are fine. This patch won't prevent those from compiling.