111

Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?

The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...

// movie object struct Movie: Decodable, Identifiable { let id: String let title: String let year: String let type: String var posterUrl: String private enum CodingKeys: String, CodingKey { case id = "imdbID" case title = "Title" case year = "Year" case type = "Type" case posterUrl = "Poster" } } 
// root content list view that navigates to the detail view struct ContentView : View { var movies: [Movie] var body: some View { NavigationView { List(movies) { movie in NavigationButton(destination: MovieDetail(movie: movie)) { MovieRow(movie: movie) } } .navigationBarTitle(Text("Star Wars Movies")) } } } 
// detail view that needs to make the asynchronous call struct MovieDetail : View { let movie: Movie @State var imageObject = BoundImageObject() var body: some View { HStack(alignment: .top) { VStack { Image(uiImage: imageObject.image) .scaledToFit() Text(movie.title) .font(.subheadline) } } } } 

6 Answers 6

139

We can achieve this using view modifier.

  1. Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier { @State private var didLoad = false private let action: (() -> Void)? init(perform action: (() -> Void)? = nil) { self.action = action } func body(content: Content) -> some View { content.onAppear { if didLoad == false { didLoad = true action?() } } } } 
  1. Create View extension:
extension View { func onLoad(perform action: (() -> Void)? = nil) -> some View { modifier(ViewDidLoadModifier(perform: action)) } } 
  1. Use like this:
struct SomeView: View { var body: some View { VStack { Text("HELLO!") }.onLoad { print("onLoad") } } } 
Sign up to request clarification or add additional context in comments.

9 Comments

@Kyle I've updated my solution. Now it should work because State keeps it's value when view is changed.
this answer needs to be upvoted more as it's the only one that really addresses the question
This is brilliant!! Thank you!!! This should go into SwiftUI natively.
I agree that this is the best solution of those listed here, but it staill has the issue that the view must first appear in order to perform the onLoad function. On an iPad, you can't use this to select the first item in a list on the left navigation pane, because it is not initially visible.
Nice answer! Though I would name this ViewFirstAppearModifier, to better transmit the intent and behaviour.
|
73

I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.

Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.

struct SearchView : View { @State private var query: String = "Swift" @EnvironmentObject var repoStore: ReposStore var body: some View { NavigationView { List { TextField($query, placeholder: Text("type something..."), onCommit: fetch) ForEach(repoStore.repos) { repo in RepoRow(repo: repo) } }.navigationBarTitle(Text("Search")) }.onAppear(perform: fetch) } private func fetch() { repoStore.fetch(matching: query) } } 
import SwiftUI import Combine class ReposStore: BindableObject { var repos: [Repo] = [] { didSet { didChange.send(self) } } var didChange = PassthroughSubject<ReposStore, Never>() let service: GithubService init(service: GithubService) { self.service = service } func fetch(matching query: String) { service.search(matching: query) { [weak self] result in DispatchQueue.main.async { switch result { case .success(let repos): self?.repos = repos case .failure: self?.repos = [] } } } } } 

Credit to: Majid Jabrayilov

6 Comments

Correct me if i'm wrong but using fetch in onAppear causes network request on each time the view is appeared. (e.g in a TabView ).
I really hope there is a better way. I've seen the advice to use onAppear to select the first item in a list, for example. This strategy is flawed, because on an iPad, the left navigation panel is hidden by default. There needs to be a way to do some work on load of the view regardless of its being visible.
onAppear is more like viewWillAppear or viewDidAppear. The question was about viewDidLoad.
@Armin did you find any solution ?
|
23

Fully updated for Xcode 11.2, Swift 5.0

I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.

As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:

struct ContentView: View { var body: some View { NavigationView { VStack { NavigationLink(destination: DetailView()) { Text("Hello World") } } }.onAppear { print("ContentView appeared!") }.onDisappear { print("ContentView disappeared!") } } } 

ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear

4 Comments

The question was about viewDidLoad not viewDidAppear or viewWillAppear.
@CharlieFish He said that onAppear is equivalent to viewDidLoad. I'm not sure about that, but why there isn't an official answer from Apple on this?
This seems reasonable to me, but I'm not exactly sure if setting stuff in the creation of a view equals to loadView or viewDidLoad. Besides, SwiftUI View and UIKit View have quite different lifecycle (the former often gets recreated), so maybe there is no direct equivalent of viewDidLoad in a SwiftUI View.
This answer is not relevant though
15

I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.

Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.

3 Comments

One thing to keep in mind though: init() is called by its parent when the parent itself is loaded. And furthermore, the parent will init all its potential children, even though they never gets loaded. So it's not really viewDidLoad() either, thought it's only called once.
I agree with that, init is not the exact ViewDidLoad(), but it is the best alternative. @turingtested
The init method is called when view is initialized or created but view is not loaded and put inside the UI hierarchy (viewDidLoad). The init method is called every time the parent is inited, which could cause unexpected behaviors as stated in this article. swiftbysundell.com/articles/…
0

We could use isPresented environment value to acheive this behaviour.

Comments

-1
var body: some View { NavigationView { List(viewModel.photos) { photo in Text(photo.photographer) } }.task { await viewModel.fetchPhotos() } } 

The task is called when view is shown

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.