how to set a View as a parameter in SwiftUI - ios

I am trying to provide an option through a Configuration struct to show a custom view. I am not sure how to accomplish this. I tried a number of options but I keep getting compile errors. May be my approach to do this is wrong! I appreciate your help!
Here is what I tried:
configuration struct that provides options to set color, width and custom content, etc...
struct Configuration {
var color: Color
....
var customView: (([String])->AnyView?) = nil // <- this compiles but not sure if this is the right way to do it!
...
}
defining my custom view:
struct CustomView: View {
var values: [String]
var body: some View {
VStack {
ForEach(values.indices, id:\.self) { index in
Text(values[index])
}
}
}
}
Here is how I am using it and where I get the compiler error:
struct ContentView: View {
var config = Configuration()
config.customView = CustomView(["A", "B"]) // <-- Error: Cannot assign value of type CustomView to type ([String]) -> AnyView
var body: some View {
VStack {
.... // display other content
}
.overlay(showCustomView())
}
#ViewBuilder
private func showCustomView() -> some View {
if let customContent = config.customView {
customContent()
} else {
EmptyView()
}
}
}

Here is possible solution (as far as I understood your intention) - you need generics in configuration to be able to use it with any view.
Here is a demo. Prepared with Xcode 12.4.
struct Configuration<V: View> { // << view dependent
var color: Color
var customView: (([String]) -> V?)? = nil // << view builder
}
struct CustomView: View { // no changes
var values: [String]
var body: some View {
VStack {
ForEach(values.indices, id:\.self) { index in
Text(values[index])
}
}
}
}
struct ContentView: View {
var config = Configuration(color: .black) { // << create config !!
CustomView(values: $0) // ... and specify view !!
}
var body: some View {
VStack {
Text("")
}
.overlay(showCustomView())
}
#ViewBuilder
private func showCustomView() -> some View {
if let customContent = config.customView {
customContent(["A", "B"]) // << inject params !!
} else {
EmptyView()
}
}
}

Related

Is there a way to let the user decide between two ListStyles in SwiftUI

What I am trying to accomplish is a list which can change its style based on the user's preference.
I have a #AppStorage property which can be changed in the settings to use .plain or .insetGrouped in the listStyle modifier.
I have tried using a ternary operator like in the code below, but I get a type mismatch error. Here's my code:
import SwiftUI
struct ContentView: View {
#AppStorage("listStyle") private var listStyle: Bool = false
var body: some View {
NavigationStack {
List {
ForEach(1...10, id: \.self) { i in
Section {
Text("Item \(i)")
}
}
}
.listStyle(listStyle ? .plain : .insetGrouped)
}
}
}
You can use a custom ViewModifier to apply the appropriate list style based on the boolean like this:
struct ContentView: View {
#AppStorage("listStyle") private var isListPlain: Bool = false
var body: some View {
NavigationStack {
List {
ForEach(1...10, id: \.self) { i in
Section {
Text("Item \(i)")
}
}
}
.myListStyle(isListPlain: isListPlain)
}
}
}
struct MyListViewModifier : ViewModifier {
let isListPlain : Bool
func body(content: Content) -> some View {
if(isListPlain){
content.listStyle(.plain)
}else{
content.listStyle(.insetGrouped)
}
}
}
extension View {
func myListStyle(isListPlain : Bool) -> some View {
modifier(MyListViewModifier(isListPlain: isListPlain))
}
}

SwiftUI - "Argument passed to call that takes no arguments"?

I have an issue with the coding for my app, where I want to be able to scan a QR and bring it to the next page through navigation link. Right now I am able to scan a QR code and get a link but that is not a necessary function for me. Below I attached my code and got the issue "Argument passed to call that takes no arguments", any advice or help would be appreciated :)
struct QRCodeScannerExampleView: View {
#State private var isPresentingScanner = false
#State private var scannedCode: String?
var body: some View {
VStack(spacing: 10) {
if let code = scannedCode {
//error below
NavigationLink("Next page", destination: PageThree(scannedCode: code), isActive: .constant(true)).hidden()
}
Button("Scan Code") {
isPresentingScanner = true
}
Text("Scan a QR code to begin")
}
.sheet(isPresented: $isPresentingScanner) {
CodeScannerView(codeTypes: [.qr]) { response in
if case let .success(result) = response {
scannedCode = result.string
isPresentingScanner = false
}
}
}
}
}
Page Three Code
import SwiftUI
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
struct PageThree_Previews: PreviewProvider {
static var previews: some View {
PageThree()
}
}
You forgot property:
struct PageThree: View {
var scannedCode: String = "" // << here !!
var body: some View {
Text("Code: " + scannedCode)
}
}
You create your PageThree View in two ways, One with scannedCode as a parameter, one with no params.
PageThree(scannedCode: code)
PageThree()
Meanwhile, you defined your view with no initialize parameters
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
For your current definition, you only can use PageThree() to create your view. If you want to pass value while initializing, change your view implementation and consistently using one kind of initializing method.
struct PageThree: View {
var scannedCode: String
var body: some View {
Text(scannedCode)
}
}
or
struct PageThree: View {
private var scannedCode: String
init(code: String) {
scannedCode = code
}
var body: some View {
Text(scannedCode)
}
}
This is basic OOP, consider to learn it well before jump-in to development.
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

How To Switch A To A View In SwiftUI? [duplicate]

I'm trying to figure out the correct way to conditionally include a view with swiftui. I wasn't able to use the if directly inside of a view and had to use a
stack view to do it.
This works but there seems like there would be a cleaner way.
var body: some View {
HStack() {
if keychain.get("api-key") != nil {
TabView()
} else {
LoginView()
}
}
}
The simplest way to avoid using an extra container like HStack is to annotate your body property as #ViewBuilder, like this:
#ViewBuilder
var body: some View {
if user.isLoggedIn {
MainView()
} else {
LoginView()
}
}
I needed to embed a view inside another conditionally, so I ended up creating a convenience if function:
extension View {
#ViewBuilder
func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
if conditional {
content(self)
} else {
self
}
}
}
This does return an AnyView, which is not ideal but feels like it is technically correct because you don't really know the result of this during compile time.
In my case, I needed to embed the view inside a ScrollView, so it looks like this:
var body: some View {
VStack() {
Text("Line 1")
Text("Line 2")
}
.if(someCondition) { content in
ScrollView(.vertical) { content }
}
}
But you could also use it to conditionally apply modifiers too:
var body: some View {
Text("Some text")
.if(someCondition) { content in
content.foregroundColor(.red)
}
}
UPDATE: Please read the drawbacks of using conditional modifiers before using this: https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/
You didn't include it in your question but I guess the error you're getting when going without the stack is the following?
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
The error gives you a good hint of what's going on but in order to understand it, you need to understand the concept of opaque return types. That's how you call the types prefixed with the some keyword. I didn't see any Apple engineers going deep into that subject at WWDC (maybe I missed the respective talk?), which is why I did a lot of research myself and wrote an article on how these types work and why they are used as return types in SwiftUI.
🔗 What’s this “some” in SwiftUI?
There is also a detailed technical explanation in another
🔗 Stackoverflow post on opaque result types
If you want to fully understand what's going on I recommend reading both.
As a quick explanation here:
General Rule:
Functions or properties with an opaque result type (some Type)
must always return the same concrete type.
In your example, your body property returns a different type, depending on the condition:
var body: some View {
if someConditionIsTrue {
TabView()
} else {
LoginView()
}
}
If someConditionIsTrue, it would return a TabView, otherwise a LoginView. This violates the rule which is why the compiler complains.
If you wrap your condition in a stack view, the stack view will include the concrete types of both conditional branches in its own generic type:
HStack<ConditionalContent<TabView, LoginView>>
As a consequence, no matter which view is actually returned, the result type of the stack will always be the same and hence the compiler won't complain.
💡 Supplemental:
There is actually a view component SwiftUI provides specifically for this use case and it's actually what stacks use internally as you can see in the example above:
ConditionalContent
It has the following generic type, with the generic placeholder automatically being inferred from your implementation:
ConditionalContent<TrueContent, FalseContent>
I recommend using that view container rather that a stack because it makes its purpose semantically clear to other developers.
Anyway, the issue still exists.
Thinking mvvm-like all examples on that page breaks it.
Logic of UI contains in View.
In all cases is not possible to write unit-test to cover logic.
PS. I am still can't solve this.
UPDATE
I am ended with solution,
View file:
import SwiftUI
struct RootView: View {
#ObservedObject var viewModel: RatesListViewModel
var body: some View {
viewModel.makeView()
}
}
extension RatesListViewModel {
func makeView() -> AnyView {
if isShowingEmpty {
return AnyView(EmptyListView().environmentObject(self))
} else {
return AnyView(RatesListView().environmentObject(self))
}
}
}
Based on the comments I ended up going with this solution that will regenerate the view when the api key changes by using #EnvironmentObject.
UserData.swift
import SwiftUI
import Combine
import KeychainSwift
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
let keychain = KeychainSwift()
var apiKey : String? {
get {
keychain.get("api-key")
}
set {
if let newApiKey : String = newValue {
keychain.set(newApiKey, forKey: "api-key")
} else {
keychain.delete("api-key")
}
didChange.send(self)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView : View {
#EnvironmentObject var userData: UserData
var body: some View {
Group() {
if userData.apiKey != nil {
TabView()
} else {
LoginView()
}
}
}
}
Another approach using ViewBuilder (which relies on the mentioned ConditionalContent)
buildEither + optional
import PlaygroundSupport
import SwiftUI
var isOn: Bool?
struct TurnedOnView: View {
var body: some View {
Image(systemName: "circle.fill")
}
}
struct TurnedOffView: View {
var body: some View {
Image(systemName: "circle")
}
}
struct ContentView: View {
var body: some View {
ViewBuilder.buildBlock(
isOn == true ?
ViewBuilder.buildEither(first: TurnedOnView()) :
ViewBuilder.buildEither(second: TurnedOffView())
)
}
}
let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView
(There's also buildIf, but I couldn't figure out its syntax yet. ¯\_(ツ)_/¯)
One could also wrap the result View into AnyView
import PlaygroundSupport
import SwiftUI
let isOn: Bool = false
struct TurnedOnView: View {
var body: some View {
Image(systemName: "circle.fill")
}
}
struct TurnedOffView: View {
var body: some View {
Image(systemName: "circle")
}
}
struct ContentView: View {
var body: AnyView {
isOn ?
AnyView(TurnedOnView()) :
AnyView(TurnedOffView())
}
}
let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView
But it kinda feels wrong...
Both examples produce the same result:
I chose to solve this by creating a modifier that makes a view "visible" or "invisible". The implementation looks like the following:
import Foundation
import SwiftUI
public extension View {
/**
Returns a view that is visible or not visible based on `isVisible`.
*/
func visible(_ isVisible: Bool) -> some View {
modifier(VisibleModifier(isVisible: isVisible))
}
}
fileprivate struct VisibleModifier: ViewModifier {
let isVisible: Bool
func body(content: Content) -> some View {
Group {
if isVisible {
content
} else {
EmptyView()
}
}
}
}
Then to use it to solve your example, you would simply invert the isVisible value as seen here:
var body: some View {
HStack() {
TabView().visible(keychain.get("api-key") != nil)
LoginView().visible(keychain.get("api-key") == nil)
}
}
I have considered wrapping this into some kind of an "If" view that would
take two views, one when the condition is true and one when the condition is
false, but I decided that my present solution is both more general and more
readable.
Extension with the condition param works well for me (iOS 14):
import SwiftUI
extension View {
func showIf(condition: Bool) -> AnyView {
if condition {
return AnyView(self)
}
else {
return AnyView(EmptyView())
}
}
}
Example usage:
ScrollView { ... }.showIf(condition: shouldShow)
If the error message is
Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
Just hide the complexity of the control flow from the ViewBuilder:
This works:
struct TestView: View {
func hiddenComplexControlflowExpression() -> Bool {
// complex condition goes here, like "if let" or "switch"
return true
}
var body: some View {
HStack() {
if hiddenComplexControlflowExpression() {
Text("Hello")
} else {
Image("test")
}
if hiddenComplexControlflowExpression() {
Text("Without else")
}
}
}
}
Previous answers were correct, however, I would like to mention, you may use optional views inside you HStacks. Lets say you have an optional data eg. the users address. You may insert the following code:
// works!!
userViewModel.user.address.map { Text($0) }
Instead of the other approach:
// same logic, won't work
if let address = userViewModel.user.address {
Text(address)
}
Since it would return an Optional text, the framework handles it fine. This also means, using an expression instead of the if statement is also fine, like:
// works!!!
keychain.get("api-key") != nil ? TabView() : LoginView()
In your case, the two can be combined:
keychain.get("api-key").map { _ in TabView() } ?? LoginView()
Using beta 4
I extended #gabriellanata's answer for up to two conditions. You can add more if needed. You use it like this:
Text("Hello")
.if(0 == 1) { $0 + Text("World") }
.elseIf(let: Int("!")?.description) { $0 + Text($1) }
.else { $0.bold() }
The code:
extension View {
func `if`<TrueContent>(_ condition: Bool, #ViewBuilder transform: #escaping (Self) -> TrueContent)
-> ConditionalWrapper1<Self, TrueContent> where TrueContent: View {
ConditionalWrapper1<Self, TrueContent>(content: { self },
conditional: Conditional<Self, TrueContent>(condition: condition,
transform: transform))
}
func `if`<TrueContent: View, Item>(`let` item: Item?, #ViewBuilder transform: #escaping (Self, Item) -> TrueContent)
-> ConditionalWrapper1<Self, TrueContent> {
if let item = item {
return self.if(true, transform: {
transform($0, item)
})
} else {
return self.if(false, transform: {
transform($0, item!)
})
}
}
}
struct Conditional<Content: View, Trans: View> {
let condition: Bool
let transform: (Content) -> Trans
}
struct ConditionalWrapper1<Content: View, Trans1: View>: View {
var content: () -> Content
var conditional: Conditional<Content, Trans1>
func elseIf<Trans2: View>(_ condition: Bool, #ViewBuilder transform: #escaping (Content) -> Trans2)
-> ConditionalWrapper2<Content, Trans1, Trans2> {
ConditionalWrapper2(content: content,
conditionals: (conditional,
Conditional(condition: condition,
transform: transform)))
}
func elseIf<Trans2: View, Item>(`let` item: Item?, #ViewBuilder transform: #escaping (Content, Item) -> Trans2)
-> ConditionalWrapper2<Content, Trans1, Trans2> {
let optionalConditional: Conditional<Content, Trans2>
if let item = item {
optionalConditional = Conditional(condition: true) {
transform($0, item)
}
} else {
optionalConditional = Conditional(condition: false) {
transform($0, item!)
}
}
return ConditionalWrapper2(content: content,
conditionals: (conditional, optionalConditional))
}
func `else`<ElseContent: View>(#ViewBuilder elseTransform: #escaping (Content) -> ElseContent)
-> ConditionalWrapper2<Content, Trans1, ElseContent> {
ConditionalWrapper2(content: content,
conditionals: (conditional,
Conditional(condition: !conditional.condition,
transform: elseTransform)))
}
var body: some View {
Group {
if conditional.condition {
conditional.transform(content())
} else {
content()
}
}
}
}
struct ConditionalWrapper2<Content: View, Trans1: View, Trans2: View>: View {
var content: () -> Content
var conditionals: (Conditional<Content, Trans1>, Conditional<Content, Trans2>)
func `else`<ElseContent: View>(#ViewBuilder elseTransform: (Content) -> ElseContent) -> some View {
Group {
if conditionals.0.condition {
conditionals.0.transform(content())
} else if conditionals.1.condition {
conditionals.1.transform(content())
} else {
elseTransform(content())
}
}
}
var body: some View {
self.else { $0 }
}
}
How about that?
I have a conditional contentView, which either is a text or an icon. I solved the problem like this. Comments are very appreciated, since I don't know if this is really "swifty" or just a "hack", but it works:
private var contentView : some View {
switch kind {
case .text(let text):
let textView = Text(text)
.font(.body)
.minimumScaleFactor(0.5)
.padding(8)
.frame(height: contentViewHeight)
return AnyView(textView)
case .icon(let iconName):
let iconView = Image(systemName: iconName)
.font(.title)
.frame(height: contentViewHeight)
return AnyView(iconView)
}
}
Use Group instead of HStack
var body: some View {
Group {
if keychain.get("api-key") != nil {
TabView()
} else {
LoginView()
}
}
}
Here’s a very simple to use modifier which uses a boolean test to decide if a view will be rendered. Unlike other solutions posted here it doesn’t rely on the use of ÀnyView. This is how to use it:
var body: some View {
VStack {
FooView()
.onlyIf(someCondition)
}
}
This reads nicer than the default if … then construct as it removes the additional indentation.
To replace an if … then … else construct, this is the obvious solution:
var body: some View {
VStack {
FooView()
.onlyIf(someCondition)
BarView()
.onlyIf(!someCondition)
}
}
This is the definition of the onlyIf modifier:
struct OnlyIfModifier: ViewModifier {
var condition: Bool
func body(content: Content) -> some View {
if condition {
content
}
}
}
extension View {
func onlyIf(_ condition: Bool) -> some View {
modifier(OnlyIfModifier(condition: condition))
}
}
Give it a try – it will surely clean up your code and improve overall readability.
If you want to navigate to two different views using NavigationLink, you can navigate using ternary operator.
let profileView = ProfileView()
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
let otherProfileView = OtherProfileView(data: user)
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
NavigationLink(destination: profileViewModel.userName == user.userName ? AnyView(profileView) : AnyView(otherProfileView)) {
HStack {
Text("Navigate")
}
}

SwiftUI NavigationLink loads destination view immediately, without clicking

With following code:
struct HomeView: View {
var body: some View {
NavigationView {
List(dataTypes) { dataType in
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
}
}
}
}
What's weird, when HomeView appears, NavigationLink immediately loads the AnotherView. As a result, all AnotherView dependencies are loaded as well, even though it's not visible on the screen yet. The user has to click on the row to make it appear.
My AnotherView contains a DataSource, where various things happen. The issue is that whole DataSource is loaded at this point, including some timers etc.
Am I doing something wrong..? How to handle it in such way, that AnotherView gets loaded once the user presses on that HomeViewRow?
The best way I have found to combat this issue is by using a Lazy View.
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
Then the NavigationLink would look like this. You would place the View you want to be displayed inside ()
NavigationLink(destination: NavigationLazyView(DetailView(data: DataModel))) { Text("Item") }
EDIT: See #MwcsMac's answer for a cleaner solution which wraps View creation inside a closure and only initializes it once the view is rendered.
It takes a custom ForEach to do what you are asking for since the function builder does have to evaluate the expression
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
for each visible row to be able to show HomeViewRow(dataType:), in which case AnotherView() must be initialized too.
So to avoid this a custom ForEach is necessary.
import SwiftUI
struct LoadLaterView: View {
var body: some View {
HomeView()
}
}
struct DataType: Identifiable {
let id = UUID()
var i: Int
}
struct ForEachLazyNavigationLink<Data: RandomAccessCollection, Content: View, Destination: View>: View where Data.Element: Identifiable {
var data: Data
var destination: (Data.Element) -> (Destination)
var content: (Data.Element) -> (Content)
#State var selected: Data.Element? = nil
#State var active: Bool = false
var body: some View {
VStack{
NavigationLink(destination: {
VStack{
if self.selected != nil {
self.destination(self.selected!)
} else {
EmptyView()
}
}
}(), isActive: $active){
Text("Hidden navigation link")
.background(Color.orange)
.hidden()
}
List{
ForEach(data) { (element: Data.Element) in
Button(action: {
self.selected = element
self.active = true
}) { self.content(element) }
}
}
}
}
}
struct HomeView: View {
#State var dataTypes: [DataType] = {
return (0...99).map{
return DataType(i: $0)
}
}()
var body: some View {
NavigationView{
ForEachLazyNavigationLink(data: dataTypes, destination: {
return AnotherView(i: $0.i)
}, content: {
return HomeViewRow(dataType: $0)
})
}
}
}
struct HomeViewRow: View {
var dataType: DataType
var body: some View {
Text("Home View \(dataType.i)")
}
}
struct AnotherView: View {
init(i: Int) {
print("Init AnotherView \(i.description)")
self.i = i
}
var i: Int
var body: some View {
print("Loading AnotherView \(i.description)")
return Text("hello \(i.description)").onAppear {
print("onAppear AnotherView \(self.i.description)")
}
}
}
I had the same issue where I might have had a list of 50 items, that then loaded 50 views for the detail view that called an API (which resulted in 50 additional images being downloaded).
The answer for me was to use .onAppear to trigger all logic that needs to be executed when the view appears on screen (like setting off your timers).
struct AnotherView: View {
var body: some View {
VStack{
Text("Hello World!")
}.onAppear {
print("I only printed when the view appeared")
// trigger whatever you need to here instead of on init
}
}
}
For iOS 14 SwiftUI.
Non-elegant solution for lazy navigation destination loading, using view modifier, based on this post.
extension View {
func navigate<Value, Destination: View>(
item: Binding<Value?>,
#ViewBuilder content: #escaping (Value) -> Destination
) -> some View {
return self.modifier(Navigator(item: item, content: content))
}
}
private struct Navigator<Value, Destination: View>: ViewModifier {
let item: Binding<Value?>
let content: (Value) -> Destination
public func body(content: Content) -> some View {
content
.background(
NavigationLink(
destination: { () -> AnyView in
if let value = self.item.wrappedValue {
return AnyView(self.content(value))
} else {
return AnyView(EmptyView())
}
}(),
isActive: Binding<Bool>(
get: { self.item.wrappedValue != nil },
set: { newValue in
if newValue == false {
self.item.wrappedValue = nil
}
}
),
label: EmptyView.init
)
)
}
}
Call it like this:
struct ExampleView: View {
#State
private var date: Date? = nil
var body: some View {
VStack {
Text("Source view")
Button("Send", action: {
self.date = Date()
})
}
.navigate(
item: self.$date,
content: {
VStack {
Text("Destination view")
Text($0.debugDescription)
}
}
)
}
}
I was recently struggling with this issue (for a navigation row component for forms), and this did the trick for me:
#State private var shouldShowDestination = false
NavigationLink(destination: DestinationView(), isActive: $shouldShowDestination) {
Button("More info") {
self.shouldShowDestination = true
}
}
Simply wrap a Button with the NavigationLink, which activation is to be controlled with the button.
Now, if you're to have multiple button+links within the same view, and not an activation State property for each, you should rely on this initializer
/// Creates an instance that presents `destination` when `selection` is set
/// to `tag`.
public init<V>(destination: Destination, tag: V, selection: Binding<V?>, #ViewBuilder label: () -> Label) where V : Hashable
https://developer.apple.com/documentation/swiftui/navigationlink/3364637-init
Along the lines of this example:
struct ContentView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) {
Button("Tap to show second") {
self.selection = "Second"
}
}
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) {
Button("Tap to show third") {
self.selection = "Third"
}
}
}
.navigationBarTitle("Navigation")
}
}
}
More info (and the slightly modified example above) taken from https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui (under "Programmatic navigation").
Alternatively, create a custom view component (with embedded NavigationLink), such as this one
struct FormNavigationRow<Destination: View>: View {
let title: String
let destination: Destination
var body: some View {
NavigationLink(destination: destination, isActive: $shouldShowDestination) {
Button(title) {
self.shouldShowDestination = true
}
}
}
// MARK: Private
#State private var shouldShowDestination = false
}
and use it repeatedly as part of a Form (or List):
Form {
FormNavigationRow(title: "One", destination: Text("1"))
FormNavigationRow(title: "Two", destination: Text("2"))
FormNavigationRow(title: "Three", destination: Text("3"))
}
In the destination view you should listen to the event onAppear and put there all code that needs to be executed only when the new screen appears. Like this:
struct DestinationView: View {
var body: some View {
Text("Hello world!")
.onAppear {
// Do something important here, like fetching data from REST API
// This code will only be executed when the view appears
}
}
}

SwiftUI View - viewDidLoad()?

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)
}
}
}
}
We can achieve this using view modifier.
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?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
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
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
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.

Resources