Where to put my controller logic?

Hi,


I just started playing around with SwiftUI and have stumbled over something: in my "regular" apps, my view controllers take care of loading data from a web service (triggered by a function called in the ViewDidLoad() part) and then hand the information over to the views.


If I understand the concept of SwiftUI correctly, this whole piece of loading data is no longer managed in the View files but somewhere outside.


So in my app now I have a class of WarehouseOrder and created another class called WarehouseOrderController that is responsible for loading the data from the server and storing it in an array (happens in function loadData() called by the init() of that class)


@Published var warehouseOrders:[WarehouseOrder] = []


From my main menu I call the View WarehouseOrderOverview via a NavigationLink


struct WarehouseOrderOverview: View {
  @EnvironmentObject var settingStore: SettingStore

  var warehouseOrderController = WarehouseOrderController()

    var body: some View {
  List(warehouseOrderController.warehouseOrders){ warehouseOrder in
  Text(warehouseOrder.orderRef)
  }
    }
}


What I don't get is that when I start the app, it is triggering the init() of WarehouseOrderController already when I am in the main menu. How can I change it that the data only gets loaded when I actally trigger the NavigationLink to WarehouseOrderOverview?



Max

Post not yet marked as solved Up vote post of bobandsee Down vote post of bobandsee
3.6k views

Replies

What I don't get is that when I start the app, it is triggering the init() of WarehouseOrderController already when I am in the main menu.

What is this main menu ?


How can I change it that the data only gets loaded when I actally trigger the NavigationLink to WarehouseOrderOverview?

If you put the call to WarehouseOrderController() in the destination, it should be called only at that time.

But do you need a controller, or do you need to set some data ?


If you need to pass the array content back to the"main" view (the one that calls the link):

- define a var for the array in "main" view

@State var myArray : [SomeType] = []

- define a binding var in WarehouseOrderOverview

@Binding var readArray: [SomeType]

- pass the var in link

NavigationLink(destination: WarehouseOrderOverview(readArray: self.$myArray))

- In WarehouseOrderOverview

Read data into myArray (with the code that was in Controller)


Hope I did not get confused between the names of the different views.

Note: the name warehouseOrderController is misleading, as it is not really a controller

hi bobandsee,


i'll second your uneasiness in trying to remove the C from MVC ⚠


but on the point of your question, i'd suggest the series of tutorials from Paul Hudson (hackingwithswift.com) with title "Cupcake Corner SwiftUI Tutorial." there are 8 parts, all of which are available on YouTube, and i think you'll see many similarities to what i think you're trying to do.


hope that helps,

DMG

My recommendation would be to remove the call to loadData() from your controller's init() method. While convenient for single-use items, it can often lead to complex behaviors that are difficult to reason about—such as you've just found. A pattern I'd recommend for your use fould be to have init() just set up the internals, have a public loadData() method that can be called explicitly, and then have a convienience class method that does both—WarehouseOrderController.autoLoadingController() for example.


Having done that, the simplest way to do what you need would be to use a `didAppear { }` block in your view that calls `warehouseOrderController.loadData()`. You'd then need to make your WarehouseOrderController conform to ObservableObject, and change its variable declaration to use the @ObservedObject attribute, so that SwiftUI can tell that you want to update when its published properties change.


You'd end up with a view something like this:


struct WarehouseOrderOverview: View {
    @EnvironmentObject var settingsStore: SettingsStore
    @ObservedObject var warehouseOrderController = WarehouseOrderController() // just init(), not loading

    var body: some View {
        List(warehouseOrderController.warehouseOrders) { order in
            // order view here
        }
        .onAppear {
            // load the data when first displayed.
            warehouseOrderController.loadData()
        }
    }
}


Since your warehouseOrderController is marked with @ObservableObject, when you call warehouseOrderController.warehouseOrders in your List initializer, SwiftUI will take notice and will cause your view body to be re-fetched when that property changes. Without the @ObservableObject keyword, SwiftUI doesn't know to look, and will not trigger anything when the load completes.

Hi Jim,


Thanks for the advise, that seems to work now BUT when running it the console logs this message over and over again


2020-01-19 21:01:50.535221-0500 WMS Toolbox[15833:27982195] [SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

and Xcode highllights the code segment where I add entries to the array with


self.warehouseOrders.append(warehouseOrder)


Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.


I guess I get that because I am fetching the data via an URLsession and that is managed in a different thread? So where exactly would I put the ".receive(on: RunLoop.main)" piece now? 🙂

Ok, so I might have fixed (or broken?) it by changing it to


DispatchQueue.main.async {
  self.warehouseOrders.append(warehouseOrder)
  }


And now the view load fine the 1st time. If I now go back to the main menu and tap the button again to go back to the view, I get this error


[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x106104400; baseClass = UITableView; frame = (0 0; 0 0); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x2810383f0>; layer = <CALayer: 0x281ece380>; contentOffset: {0, 0}; contentSize: {0, 0}; adjustedContentInset: {0, 0, 0, 0}; dataSource: <_TtGC7SwiftUIP10$1f7ddd2b419ListCoreCoordinatorGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x105d6c370>>

2020-01-19 23:08:57.856014-0500 WMS Toolbox[1286:554768] *** Assertion failure in -[_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore/UIKit-3901.4.2/UITableView.m:2040

2020-01-19 23:08:57.857277-0500 WMS Toolbox[1286:554768] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete section 0, but there are only 0 sections before the update'


My call in the ContentView() is


NavigationLink(destination: WarehouseOrderOverview().navigationBarTitle("Orders")) {
  MenuButtonNavigation(title: "Order overview", color: Color.gray, icon: "doc.text.magnifyingglass").padding(.top)
  }


My WarehouseOrderOverview looks like


struct WarehouseOrderOverview: View {
  @EnvironmentObject var settingStore: SettingStore

  @ObservedObject var warehouseOrderController = WarehouseOrderController()

  var body: some View {

  List(warehouseOrderController.warehouseOrders){ warehouseOrder in
  VStack{
  OrderTableRow(warehouseOrder: warehouseOrder)
  }
  }.onAppear{
  self.warehouseOrderController.loadData()
  }
  }
}


Any ideas?