0
votes

26-07-19

I'll update my code as I'm making progress watching the WWDC video's. My data model is:

struct Egg: Identifiable {
    var id = UUID()
    var thumbnailImage: String
    var day: String
    var date: String
    var text: String
    var imageDetail: String
    var weight: Double
}

#if DEBUG
let testData = [

    Egg(thumbnailImage: "Dag-1", day: "1.circle", date: "7 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-1", weight: 35.48),

    Egg(thumbnailImage: "Dag-2", day: "2.circle", date: "8 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-2", weight: 35.23),

    Egg(thumbnailImage: "Dag-3", day: "3.circle", date: "9 augustus 2019", text: "Kippen leggen iedere dag een ei.", imageDetail: "Day-3", weight: 34.92)

Etc, etc
]

I've a TabbedView, a ContentView, a ContentDetail and a couple of other views (for settings etc). The code for the ContentView is:

struct ContentView : View {
    var eggs : [Egg] = []

    var body: some View {
        NavigationView {
            List(eggs) { egg in
                EggCell(egg: egg)
            }
            .padding(.top, 10.0)
            .navigationBarTitle(Text("Egg management"), displayMode: .inline)
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView(eggs: testData)
    }
}
#endif

struct EggCell : View {
    let egg: Egg

    var body: some View {

        return NavigationLink(destination: ContentDetail(egg: egg)) {

            ZStack {

                HStack(spacing: 8.0) {

                    Image(egg.thumbnailImage)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .padding(.leading, -25)
                        .padding(.top, -15)
                        .padding(.bottom, -15)
                        .padding(.trailing, -25)
                        .frame(width: 85, height: 61)

                    VStack {
                        Image(systemName: egg.day)
                            .resizable()
                            .frame(width: 30, height: 22)
                            .padding(.leading, -82)

                        Spacer()
                    }
                    .padding(.leading)

                    VStack {
                        Text(egg.date)
                            .font(.headline)
                            .foregroundColor(Color.gray)
                        Text(egg.weight.clean)
                            .font(.title)

                    }

                }
            }
        }
    }
}

extension Double {
    var clean: String {
        return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(format: "%.2f", self)
    }
}

The code for the ContentDetail is:

struct ContentDetail : View {
    let egg: Egg

    @State private var photo = true
    @State private var calculated = false
    @Binding var weight: Double

    var body: some View {

        VStack (alignment: .center, spacing: 10) {

            Text(egg.date)
                .font(.title)
                .fontWeight(.medium)
                .navigationBarTitle(Text(egg.date), displayMode: .inline)

            ZStack (alignment: .topLeading) {

                Image(photo ? egg.imageDetail : egg.thumbnailImage)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .background(Color.black)
                    .padding(.trailing, 0)
                    .tapAction { self.photo.toggle() }

                VStack {

                    HStack {
                        Image(systemName: egg.day)
                            .resizable()
                            .padding(.leading, 10)
                            .padding(.top, 10)
                            .frame(width: 50, height: 36)
                            .foregroundColor(.white)

                        Spacer()

                        Image(systemName: photo ?  "photo" : "wand.and.stars")
                            .resizable()
                            .padding(.trailing, 10)
                            .padding(.top, 10)
                            .frame(width: 50, height: 36)
                            .foregroundColor(.white)

                    }

                    Spacer()

                    HStack {
                        Image(systemName: "arrow.left.circle")
                            .resizable()
                            .padding(.leading, 10)
                            .padding(.bottom, 10)
                            .frame(width: 50, height: 50)
                            .foregroundColor(.white)

                        Spacer()

                        Image(systemName: "arrow.right.circle")
                            .resizable()
                            .padding(.trailing, 10)
                            .padding(.bottom, 10)
                            .frame(width: 50, height: 50)
                            .foregroundColor(.white)

                    }
                }
            }

            Text("the weight is: \(egg.weight) gram")
                .font(.headline)
                .fontWeight(.bold)

            ZStack {

                RoundedRectangle(cornerRadius: 10)
                    .padding(.top, 45)
                    .padding(.bottom, 45)
                    .border(Color.gray, width: 5)
                    .opacity(0.1)

                HStack {

                    Spacer()

                    DigitPicker(digitName: "tens", digit: $weight.tens)
                    DigitPicker(digitName: "ones", digit: $weight.ones)

                    Text(".")
                        .font(.largeTitle)
                        .fontWeight(.black)
                        .padding(.bottom, 10)

                    DigitPicker(digitName: "tenths", digit: $weight.tenths)
                    DigitPicker(digitName: "hundredths", digit: $weight.hundredths)

                    Spacer()

                    }
                }

            Toggle(isOn: $calculated) {
                Text(calculated ? "This weight is calculated." : "This weight is measured.")
            }

            Text(egg.text)
                .lineLimit(2)
                .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                .padding(.leading, 6)

            Spacer()

        }
        .padding(6)

    }
}

#if DEBUG
struct ContentDetail_Previews : PreviewProvider {
    static var previews: some View {
        NavigationView { ContentDetail(egg: testData[0]) }
    }
}
#endif


struct DigitPicker: View {
    var digitName: String
    @Binding var digit: Int

    var body: some View {
        VStack {
            Picker(selection: $digit, label: Text(digitName)) {
                ForEach(0 ... 9) {
                    Text("\($0)").tag($0)
                }
            }.frame(width: 60, height: 110).clipped()
        }
    }
}

fileprivate extension Double {
    var tens: Int {
        get { sigFigs / 1000 }
        set { replace(tens: newValue)  }
    }

    var ones: Int {
        get { (sigFigs / 100) % 10 }
        set { replace(ones: newValue) }
    }

    var tenths: Int {
        get { (sigFigs / 10) % 10 }
        set { replace(tenths: newValue) }
    }

    var hundredths: Int {
        get { sigFigs % 10 }
        set { replace(hundredths: newValue) }
    }

    private mutating func replace(tens: Int? = nil, ones: Int? = nil, tenths: Int? = nil, hundredths: Int? = nil) {
        self = Double(0
            + 1000 * (tens ?? self.tens)
            + 100 * (ones ?? self.ones)
            + 10 * (tenths ?? self.tenths)
            + (hundredths ?? self.hundredths)) / 100.0
    }

    private var sigFigs: Int {
        return Int((self * 100).rounded(.toNearestOrEven))
    }
}

The compiler errors I'm still getting are:

  1. in ContentView, beneath NavigationLink: Missing argument for parameter 'weight' in call
  2. in ContentDetail, at NavigationView: Missing argument for parameter 'weight' in call
  3. in ContentDetail, after #endif: Missing argument for parameter 'weight' in call

25-07-19

The following code is part of a List detail view. The var 'weight' is coming from the List through a 'NavigationLink' statement. In this code I declare it as '35.48', but the NavigationLink fills in its real value.

I want to make an array [3, 5, 4, 8] with the compactMap statement. That works okay in Playground. The values go to 4 different pickers (within a HStack).

import SwiftUI
import Foundation

    struct ContentDetail : View {

        var weight : Double = 35.48
        var weightArray = "\(weight)".compactMap { Int("\($0)") }

        @State var digit1 = weightArray[0] // error
        @State var digit2 = weightArray[1] // error
        @State var digit3 = weightArray[2] // error
        @State var digit4 = weightArray[3] // error

        var body: some View {

            VStack (alignment: .center, spacing: 10) {

                Text(weight)
                    .font(.title)
                    .fontWeight(.medium)

    etc etc

I get an error 'Cannot use instance member 'weightArray' within property initializer; property initializers run before 'self' is available'.

If I use the following code it works fine (for the first list element):

import SwiftUI
import Foundation

    struct ContentDetail : View {

        var weight : Double = 35.48
        var weightArray = [3, 5, 4, 8]

        @State var digit1 = 3
        @State var digit2 = 5
        @State var digit3 = 4
        @State var digit4 = 8

        var body: some View {

            VStack (alignment: .center, spacing: 10) {

                Text(weight)
                    .font(.title)
                    .fontWeight(.medium)

    etc etc

What is the correct SwiftUI approach and why?

1
I tried 'lazy var weightArray = "(weight)".compactMap { Int("($0)") }' but then the state var's still complain (even when I also make them lazy).arakweker
Are you eventually removing the weight variable into something more "stateful"? If so, why not do it now? In fact, if you believe you're headed to using any kind of model of some sort, maybe you should now and save yourself some time - include your logic there and just bind your view to it appropriately. I finally watched this video early this morning and I wonder if it may help you called The Swift behind SwiftUI: youtube.com/watch?v=2eK8voQeokk&feature=youtu.bedfd

1 Answers

3
votes

Indeed, a property initializer cannot refer to another property in the same container. You have to initialize your properties in an init instead.

struct ContentDetail: View {

    var weight: Double
    var weightArray: [Int]

    @State var digit1: Int
    @State var digit2: Int
    @State var digit3: Int
    @State var digit4: Int

    init(weight: Double) {
        self.weight = weight
        weightArray = "\(weight)".compactMap { Int("\($0)") }
        _digit1 = .init(initialValue: weightArray[0])
        _digit2 = .init(initialValue: weightArray[1])
        _digit3 = .init(initialValue: weightArray[2])
        _digit4 = .init(initialValue: weightArray[3])
    }

But I suspect you're breaking out the digits because you want to let the user edit them individually, like this:

digit editor screenshot

If that's what you want, you should not have a separate @State property for each digit. Instead, weight should be a @Binding and it should have a separate mutable property for each digit.

First, extend Double to give you access to the digits:

fileprivate extension Double {
    var tens: Int {
        get { sigFigs / 1000 }
        set { replace(tens: newValue)  }
    }

    var ones: Int {
        get { (sigFigs / 100) % 10 }
        set { replace(ones: newValue) }
    }

    var tenths: Int {
        get { (sigFigs / 10) % 10 }
        set { replace(tenths: newValue) }
    }

    var hundredths: Int {
        get { sigFigs % 10 }
        set { replace(hundredths: newValue) }
    }

    private mutating func replace(tens: Int? = nil, ones: Int? = nil, tenths: Int? = nil, hundredths: Int? = nil) {
        self = Double(0
            + 1000 * (tens ?? self.tens)
            + 100 * (ones ?? self.ones)
            + 10 * (tenths ?? self.tenths)
            + (hundredths ?? self.hundredths)) / 100.0
    }

    private var sigFigs: Int {
        return Int((self * 100).rounded(.toNearestOrEven))
    }
}

Then, change ContentDetail's weight property to be a @Binding and get rid of the other properties:

struct ContentDetail: View {
    @Binding var weight: Double

    var body: some View {
        HStack {
            DigitPicker(digitName: "tens", digit: $weight.tens)
            DigitPicker(digitName: "ones", digit: $weight.ones)
            DigitPicker(digitName: "tenths", digit: $weight.tenths)
            DigitPicker(digitName: "hundredths", digit: $weight.hundredths)
        }
    }
}

struct DigitPicker: View {
    var digitName: String
    @Binding var digit: Int

    var body: some View {
        VStack {
            Picker(selection: $digit, label: Text(digitName)) {
                ForEach(0 ... 9) {
                    Text("\($0)").tag($0)
                }
            }.frame(width: 60, height: 110).clipped()
        }
    }
}

Here's the rest of the code needed to test this in a playground, which is how I generated that image above:

import PlaygroundSupport

struct TestView: View {
    @State var weight: Double = 35.48

    var body: some View {
        VStack(spacing: 0) {
            Text("Weight: \(weight)")
            ContentDetail(weight: $weight)
                .padding()
        }
    }
}

let host = UIHostingController(rootView: TestView())
host.preferredContentSize = .init(width: 320, height: 240)
PlaygroundPage.current.liveView = host