For-in loop doesn’t update my main view realtime

I’m quite new to SwiftUI, and I have the following problem. I have a for in loop inside a computed property, that doesn’t update my view realtime, but only when the user interacts with another part of the UI. I’ll show you some part of my code, hoping you would help me solve this issue:

Status Model:

class Status: ObservableObject {
    @Published var generalData = GeneralData()
    @Published var sourceData = SourceData()
    @Published var framingData = FramingData()
    @Published var additionalViewArray: [AdditionalView] = []
    @Published var additionalViewNumber: Int = 1
}

Additional Model:

class Additional: ObservableObject {
    @Published var additionalHorizontalResolution: Int?
    @Published var additionalVerticalResolution: Int?
    @Published var additionalOffsetX: Int?
    @Published var additionalOffsetY: Int?
    @Published var additionalColor: Color = Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1))
    
    
    
    init(additionalHorizontalResolution: Int? = nil, additionalVerticalResolution: Int? = nil, additionalOffsetX: Int? = nil, additionalOffsetY: Int? = nil) {
        self.additionalHorizontalResolution = additionalHorizontalResolution
        self.additionalVerticalResolution = additionalVerticalResolution
        self.additionalOffsetX = additionalOffsetX
        self.additionalOffsetY = additionalOffsetY
    }
}

AdditionalView:

struct AdditionalView: View, Identifiable {
    @ObservedObject var data: Status
    @ObservedObject var model: Additional
    let id = UUID()
    var number: Int
        
        var createImage: NSImage? {
        if let horizontal = self.model.additionalHorizontalResolution, let vertical = self.model.additionalVerticalResolution {
            let imageSize = NSSize(width: horizontal, height: vertical)
            let imageColor = NSColor(model.additionalColor)
            let image = NSImage(size: imageSize)
            image.lockFocus()
            imageColor.setFill()
            NSBezierPath(rect: NSRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)).fill()
            image.unlockFocus()
            return image
        }
        return NSImage()
    }
       
        var body: some View {
           //Some code here...
          }

ContentView:

struct ContentView: View {
    @StateObject var status: Status
    @StateObject var additional: Additional
    var body: some View {
        HStack{
            VStack {
                SourceView(data: status, additional: additional)
                ScrollView {
                    FramingView(data: status, additional: additional)
                }
            }
            PreviewView(data: status, additional: additional)

FramingView:

struct FramingView: View {
    @ObservedObject var data: Status
    @ObservedObject var additional: Additional
        var body: some View {

                //Some code here....


        ForEach(data.additionalViewArray, id: \.id) { view in
            view
        }
        Button(action: {
            data.additionalViewNumber += 1
            data.additionalViewArray.append(AdditionalView(data: data, model: Additional(), number: data.additionalViewNumber))
        }) {
            Label("Add Format", systemImage: "plus.viewfinder")
        }

PreviewView:

struct PreviewView: View {
    
    @ObservedObject var data: Status
    @ObservedObject var additional: Additional
        
        //Some code here...

        var previewImage: NSImage? {
        let sourceColor = data.sourceData.sourceColor
        let framingColor = data.framingData.framingColor
        let NssourceColor = NSColor(sourceColor)
        let NsframingColor = NSColor(framingColor)
        
        guard let sourceHorizontalResolution = data.sourceData.sourceHorizontalResolution, let sourceVerticalResolution = data.sourceData.sourceVerticalResolution else { return placeholederText }
        let sourceSize = CGSize(width: sourceHorizontalResolution, height: sourceVerticalResolution)
        let image = NSImage(size: sourceSize)
        image.lockFocus()
        NssourceColor.setFill()
        NSBezierPath(rect: NSRect(origin: .zero, size: sourceSize)).fill()
        image.unlockFocus()
        if let framingHorizontalResolution = data.framingData.framingHorizontalResolution, let framingVerticalResolution = data.framingData.framingVerticalResolution {
            let framingSize = CGSize(width: framingHorizontalResolution, height: framingVerticalResolution)
            image.lockFocus()
            NsframingColor.setFill()
            NSBezierPath(rect: NSRect(x: (sourceSize.width - framingSize.width) / 2, y: (sourceSize.height - framingSize.height) / 2, width: framingSize.width, height: framingSize.height)).fill()
            image.unlockFocus()
            
        }
        for view in data.additionalViewArray {
            if let additional = view.createImage {
                image.lockFocus()
                additional.draw(at: NSPoint(x: (sourceSize.width - additional.size.width) / 2, y: (sourceSize.height - additional.size.height) / 2), from: NSRect.zero, operation: .sourceOver, fraction: 1.0)
                image.unlockFocus()
            }
        }
        return image
    }

        //Some code here...

       var body: some View {
        VStack(alignment: .trailing) {
        if let previewImage = previewImage {
             Image(nsImage: previewImage)
            .resizable()
            .scaledToFit()
            .frame(width: 500, height: 500)
            }

When the user press a button in the FramingView, it adds an instance of AdditionalView in an AdditionalViewArray, defined as Published in the Status model. A ForEach view shows each added AdditionalView. Each AdditionalView observe the Additional Model, in which I have some @Published vars. In the PreviewView, I have a computed property that creates an NSImage; as shown in the code, I’m using a for in loop to iterate over the instances of AdditionalView, reading values from each Additional model. When the user fills a TextField in one of the AdditionalView, the computed property doesn’t update the “Image(nsImage: previewImage)” realtime. The user would need to select another TextField (e.g the one inside the FramingView) to update it.

Could you help me to solve this?

  • 1

    Observation, in class Status, you should not have @Published var additionalViewArray: [AdditionalView] = [] where AdditionalView is a SwiftUI View. Have a look at this link, it gives you some good examples of how to manage data in your app: monitoring data

    – 




  • Thanks for your reply! I was suspecting the Array of views was not the right way to manage those View instances. What would be the right way to add an instance of a view through a button, show every instance with a ForEach, and getting data from each instance of view?

    – 

A computed property won’t trigger an update. What you need to do is subscribe to the changes of the Published property of the Additional model on which you want to update your image.

struct PreviewView: View {

    @ObservedObject var data: Status
    @ObservedObject var additional: Additional

    private let cancelables = Set<AnyCancellable>()
    
    init(data: Status, additional: Additional) {
        self.data = data
        self.additional = additional
        additional.$additionalViewNumber // replace with the property that you want to observe
            .receive(on: DispatchQueue.main)
            .sink { newValue in
                // Update image here
            }
            .store(in: &cancelables)
    }

    ...

}

Leave a Comment