SwiftUI 视图 - viewDidLoad()?

100

在视图加载后尝试加载图像时,驱动视图的模型对象(见下面的 MovieDetail)具有 urlString。由于 SwiftUI 的 View 元素没有生命周期方法(并且没有视图控制器驱动),最好的处理方式是什么?

我遇到的主要问题是无论我尝试解决问题的方式是什么(绑定对象或使用 State 变量),我的 View 直到加载后才拥有 urlString...

// 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)
            }
        }
    }
}
5个回答

117

我们可以使用视图修饰符来实现这一点。

  1. 创建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. 创建View扩展:
extension View {

    func onLoad(perform action: (() -> Void)? = nil) -> some View {
        modifier(ViewDidLoadModifier(perform: action))
    }

}
使用方式如下:
  1. 像这样使用:
struct SomeView: View {
    var body: some View {
        VStack {
            Text("HELLO!")
        }.onLoad {
            print("onLoad")
        }
    }
}

1
@Kyle,我更新了我的解决方案。现在它应该可以工作了,因为“State”在视图更改时保留其值。 - Murlakatam
4
这个回答需要得到更多的赞,因为它是唯一真正回答了这个问题的回答。 - Jandro Rojas
1
这太棒了!!谢谢你!!这应该原生地放入SwiftUI中。 - TruMan1
4
我同意这是列出的方案中最好的解决方案,但它仍然存在一个问题,即视图必须首先出现才能执行onLoad函数。在iPad上,你不能使用这个来选择左侧导航窗格中列表的第一项,因为它最初不可见。 - Tony the Tech
3
不错的回答!虽然我会将其命名为ViewFirstAppearModifier,以更好地传达意图和行为。 - Cristik
显示剩余3条评论

71

我希望这对你有所帮助。我找到了一篇名为"SwiftUI:创建实用的应用"的博客文章,其中讲述了如何在导航视图中执行onAppear操作。

具体做法是将你的服务嵌入到BindableObject中,并在视图中订阅这些更新。

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 = []
                }
            }
        }
    }
}

作者:Majid Jabrayilov


23
请纠正我如果我说错了,但是在onAppear中使用fetch会导致每次视图被显示时都进行一次网络请求。(例如在TabView中)。 - Armin
我真的希望有更好的方法。例如,我看到建议使用onAppear来选择列表中的第一项。但是这种策略存在缺陷,因为在iPad上,默认情况下左侧导航面板是隐藏的。需要有一种方法在视图加载时执行一些工作,而不管它是否可见。 - Tony the Tech
18
onAppear жӣҙзұ»дјјдәҺ viewWillAppear жҲ– viewDidAppearгҖӮй—®йўҳжҳҜе…ідәҺ viewDidLoad зҡ„гҖӮ - Charlie Fish
@Armin,你找到任何解决方案了吗? - Nirav Kotecha
这解决了我的问题 https://dev59.com/u1MI5IYBdhLWcg3wUp02#68833844 - Saeed All Gharaee
本文提供了一个很好的例子:https://swiftontap.com/view/onappear(perform:)。每当页面由于状态改变而重新渲染时,它就会被调用,因此它与视图出现或加载并不完全相同,它处于两者之间的某个位置。 - James Toomey

22

针对 Xcode 11.2,Swift 5.0 进行全面更新。

我认为 viewDidLoad() 和在主体闭包中实现代码是相同的。
SwiftUI 以 onAppear()onDisappear() 的形式为我们提供了 UIKit 的 viewDidAppear()viewDidDisappear() 的等效物。你可以将任何代码附加到这两个事件上,并且当它们发生时,SwiftUI 将执行它们。

例如,以下代码创建了两个视图,使用 onAppear()onDisappear() 打印消息,并通过导航链接在两者之间移动:

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

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


17
这个问题是关于 viewDidLoad 的,而不是 viewDidAppear 或者 viewWillAppear - Charlie Fish
@CharlieFish 他说onAppear相当于viewDidLoad。我不确定这一点,但为什么Apple官方没有对此做出回答呢? - caravana_942
这对我来说似乎是合理的,但我不确定在视图创建时设置内容是否等同于 loadViewviewDidLoad。此外,SwiftUI View 和 UIKit View 的生命周期非常不同(前者经常被重新创建),因此可能没有直接相当于 SwiftUI 视图中的 viewDidLoad - yo1995
这个答案虽然不相关。 - ace_ventura

13

我使用init()代替viewDidLoad()。我认为onApear()不是viewDidLoad()的替代品。因为onApear()在视图出现时被调用。由于您的视图可以多次出现,这将与只被调用一次的viewDidLoad()发生冲突。

想象一下拥有一个TabView。通过滑动页面,onApear()会被多次调用,但是viewDidLoad()只会被调用一次。


1
但需要记住的是:当父级被加载时,init() 会被其父级调用。此外,父级将初始化所有可能的子级,即使它们从未被加载过。因此,它并不完全等同于 viewDidLoad(),尽管它只会被调用一次。 - turingtested
我同意,init不是完全等同于ViewDidLoad(),但它是最好的替代方法。@turingtested - Saeed All Gharaee

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

任务在视图显示时被调用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接