SwiftUI View - viewDidLoad()?
Asked Answered
P

5

107

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)
            }
        }
    }
}
Polychromy answered 7/6, 2019 at 14:35 Comment(0)
N
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

Nolpros answered 7/6, 2019 at 15:1 Comment(6)
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 ).Burier
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.Pirog
onAppear is more like viewWillAppear or viewDidAppear. The question was about viewDidLoad.Alderete
@Burier did you find any solution ?Kuomintang
This resolved my issue https://mcmap.net/q/203108/-swiftui-view-viewdidloadOsburn
This article gives a good illustration: https://swiftontap.com/view/onappear(perform:). It's called whenever a page re-renders due to state changing, so it's not quite the same as the view appearing, or loading. It's somewhere in between.Granvillegranvillebarker
W
125

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")
        }
    }
}
Wadley answered 23/10, 2020 at 7:41 Comment(9)
why does this work? I would think the actionPerformed would be overwritten each time the view reloads?Albaugh
@Albaugh I've updated my solution. Now it should work because State keeps it's value when view is changed.Wadley
this answer needs to be upvoted more as it's the only one that really addresses the questionIngalls
This is brilliant!! Thank you!!! This should go into SwiftUI natively.Moonstruck
This should be the selected answerYawmeter
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.Pirog
@TonytheTech maybe in your case you can add somewhere in your view Color.clear.onAppear {} to do what you need?Wadley
Nice answer! Though I would name this ViewFirstAppearModifier, to better transmit the intent and behaviour.Accuse
rakeshchander.medium.com/… This is very helpful tooOuzel
N
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

Nolpros answered 7/6, 2019 at 15:1 Comment(6)
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 ).Burier
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.Pirog
onAppear is more like viewWillAppear or viewDidAppear. The question was about viewDidLoad.Alderete
@Burier did you find any solution ?Kuomintang
This resolved my issue https://mcmap.net/q/203108/-swiftui-view-viewdidloadOsburn
This article gives a good illustration: https://swiftontap.com/view/onappear(perform:). It's called whenever a page re-renders due to state changing, so it's not quite the same as the view appearing, or loading. It's somewhere in between.Granvillegranvillebarker
M
24

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

Migrate answered 9/1, 2020 at 6:52 Comment(4)
The question was about viewDidLoad not viewDidAppear or viewWillAppear.Alderete
@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?Slovak
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.Foliole
This answer is not relevant thoughYvoneyvonne
O
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.

Osburn answered 18/8, 2021 at 13:58 Comment(3)
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.Impulsive
I agree with that, init is not the exact ViewDidLoad(), but it is the best alternative. @ImpulsiveOsburn
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/…Tristich
O
0
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

Oxendine answered 12/10, 2023 at 7:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.