SwiftUI - How can I call a Function in my View? - ios

I am using SwiftUI/Swift for a week now, and I love it. Now I have a problem. I want to call a Function from my View, but I get this Error
Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
This is the Code:
struct testView: View {
var body: some View {
VStack {
Text("TextBox")
Text("SecondTextBox")
self.testFunction()
}
}
func testFunction() {
print("This is a Text.")
}
}
I don't get it. In other languages its much simpler and could work that way. Can anybody help me please? Swift is pretty new to me :D

Meanwhile here are the places (not all) where/how you can call a function
init() {
self.testFunction() // non always good, but can
}
var body: some View {
self.testFunction() // 1)
return VStack {
Text("TextBox")
.onTapGesture {
self.testFunction() // 2)
}
Text("SecondTextBox")
}
.onAppear {
self.testFunction() // 3)
}
.onDisappear {
self.testFunction() // 4)
}
}
... and so on

An additional method:
Testing with Swift 5.8, you can also stick in a let _ = self.testFunction().
eg
(this is extra-contrived so that it's possible to see the effect in Preview, because print() doesn't happen in Preview, at least for me)
import SwiftUI
class MyClass {
var counter = 0
}
struct ContentView: View {
var myClass: MyClass
var body: some View {
VStack {
Text("TextBox counter = \(myClass.counter)")
// v--------------------------------------------
//
let _ = self.testFunction() // compiles happily
// self.testFunction() // does not compile
//
// ^--------------------------------------------
Text("TextBox counter = \(myClass.counter)")
}
}
func testFunction() {
print("This is a Test.")
myClass.counter += 1
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(myClass: MyClass())
}
}

Using Swift 5.3 and Xcode 12.4
I use a little extension to debug inside the Views (VStack in the example), e.g. to inspect a geometryReader.size. It could be used to call any function in the View as follows:
NOTE: For debugging purposes only. I don't recommend including code like this in any production code
VStack {
Text.console("Hello, World")
Text("TEXT"
}
extension Text {
static func console<T>(_ value: T) -> EmptyView {
print("\(value)")
return EmptyView()
}
}
This will print "Hello, World" to the console.....

There is a reason, when writing in swift UI (iOS Apps), that there is a View protocol that needs to be followed. Calling functions from inside the structure is not compliant with the protocol.
The best thing you can do is the .on(insertTimeORAction here).
Read more about it here

Related

iOS 16 Binding issue (in this case Toggle)

What i have stumped across is that when i pass a Binding<> as a parameter in a ViewModel as you will see below, it doesn't always work as intended. It works in iOS 15 but not in iOS 16. This reduces the capability to create subviews to handle this without sending the entire Holder object as a #Binding down to it's subview, which i would say would be bad practice.
So i have tried to minimize this as much as possible and here is my leasy amount of code to reproduce it:
import SwiftUI
enum Loadable<T> {
case loaded(T)
case notRequested
}
struct SuperBinder {
let loadable: Binding<Loadable<ContentView.ViewModel>>
let toggler: Binding<Bool>
}
struct Holder {
var loadable: Loadable<ContentView.ViewModel> = .notRequested
var toggler: Bool = false
func generateContent(superBinder: SuperBinder) {
superBinder.loadable.wrappedValue = .loaded(.init(toggleBinder: superBinder.toggler))
}
}
struct ContentView: View {
#State var holder = Holder()
var superBinder: SuperBinder {
.init(loadable: $holder.loadable, toggler: $holder.toggler)
}
var body: some View {
switch holder.loadable {
case .notRequested:
Text("")
.onAppear(perform: {
holder.generateContent(superBinder: superBinder)
})
case .loaded(let viewModel):
Toggle("testA", isOn: viewModel.toggleBinder) // Works but doesn't update UI
Toggle("testB", isOn: $holder.toggler) // Works completly
// Pressing TestA will even toggle TestB
// TestB can be turned of directly but has to be pressed twice to turn on. Or something?
}
}
struct ViewModel {
let toggleBinder: Binding<Bool>
}
}
anyone else stumbled across the same problem? Or do i not understand how binders work?

Mock a SwiftUI view from another module

I'm trying to test a SwiftUI view that has a subview from another module in its body:
import SwiftUI
import Abond
struct ProfileView: PresentableView, LoadedView {
#State var isLoading = true
public var body: some View {
Load(self) {
AbondProfile(onSuccess: self.onSubmitSuccess)
}
}
func load() -> Binding<Bool> {
ProfileApi.getProfileAccessToken() { result in
switch result {
case .success(let response):
Abond.accessToken = response.accessToken
case .failure(let error):
print("error getting token")
}
isLoading = false
}
return $isLoading
}
func onSubmitSuccess() {
print("success")
}
}
My question is: if I want to test the lifecycle of ProfileView without the actual AbondProfile view being built, is there a way to mock that? If it were a normal method I would inject a dependency object, but I don't know how to translate that to a struct initializer.
Abond is a Swift Package, so I can't modify AbondProfile. And I'd prefer to be able to test this with as little change to my view code as possible. I'm using XCTest.
As David Wheeler said, “Any problem in computer science can be solved with another level of indirection.”
In this case, one solution is to refer to AbondProfile indirectly, through a generic type parameter. We add a type parameter to ProfileView to replace the direct use of AbondProfile:
struct ProfileView<Content: View>: PresentableView, LoadedView {
#State var isLoading = true
#ViewBuilder var content: (_ onSuccess: #escaping () -> Void) -> Content
public var body: some View {
Load(self) {
content(onSubmitSuccess)
}
}
blah blah blah
}
We don't have to change current uses of ProfileView
if we provide a default initializer that uses AbondProfile:
extension ProfileView {
init() where Content == AbondProfile {
self.init { AbondProfile(onSuccess: $0) }
}
}
struct ProductionView: View {
var body: some View {
ProfileView() // This uses AbondProfile.
}
}
And in a test, we can provide a mock view:
struct TestView: View {
var body: some View {
ProfileView { onSuccess in
Text("a travesty of a mockery of a sham of a mockery of a travesty of two mockeries of a sham")
}
}
}
I accepted the other answer because it's a more proper solution, but I found that it actually works to just redefine the struct in your test file:
import XCTest
import Abond
import SwiftUI
// Mock for Abond.AbondProfile
public struct AbondProfile: View {
static var viewDidAppearCallback: (() -> Void)?
static var submit: (() -> Void)?
public init(onSuccess: (() -> Void)? = nil) {
AbondProfile.submit = onSuccess
}
public var body: some View {
Text(Abond.accessToken)
.onAppear {
AbondProfile.viewDidAppearCallback?()
}
}
}
class ProfileViewTests: BaseViewControllerTests {
private var viewController: UIViewController?
func testSucesss() {
let viewDidAppearExpectation = XCTestExpectation(description: "View did appear")
AbondProfile.viewDidAppearCallback = { viewDidAppearExpectation.fulfill() }
MockApi.mockRequest(ProfileApi.getProfileAccessToken, response: ProfileAccessToken(accessToken:"accessToken_123"))
initialize(viewController: UIHostingController(rootView: ProfileView()))
wait(for: [viewDidAppearExpectation], timeout: 10)
XCTAssertEqual(Abond.accessToken, "accessToken_123")
AbondProfile.submit!()
// etc.
}
}
I'm aware the static variables make the test brittle – but other than that, I'd be interested to hear if there are any other reasons not to do it this way.

How to create an enum based .textStyle(.title) modifier for Text(...) components in SwiftUI?

I want to implement a modifier setting for Texts in a similar way as it already exists for Buttons.
Aka:
Button( ... )
.buttonStyle(.plain) // <-- .plain and not PlainStyle()
Problem
Of course I cannot use an opaque which is not really the same. If it would be a View I could wrap it in an AnyView but for ViewModifiers I need another solution.
Error: Function declares an opaque return type,but the return statements in its body do not have matching underlying types
Maybe it is a bonus idea to have something like a .textStyle(.title) modifier but in my eyes, it could reduce my code to write enormously.
Source
struct TitleStyle: ViewModifier {
func body(content: Content) -> some View {
...
}
}
struct BodyStyle: ViewModifier {
func body(content: Content) -> some View {
...
}
}
enum TextStyle {
case title
case body
// Error: Function declares an opaque return type,
// but the return statements in its body do not have matching underlying types
var modifier: some ViewModifier {
switch self
{
case .title: return TitleStyle()
case .body: return BodyStyle()
}
}
}
It works different way. As all this is around generics we need to restrict declarations for known concrete types.
So, having TitleStyle and BodyStyle declared and concrete, we can specify
extension ViewModifier where Self == TitleStyle {
static var title: TitleStyle { TitleStyle() }
}
extension ViewModifier where Self == BodyStyle {
static var body: BodyStyle { BodyStyle() }
}
and then declare extension to use above like
extension View {
func textStyle<Style: ViewModifier>(_ style: Style) -> some View {
ModifiedContent(content: self, modifier: style)
}
}
so as a result we can do as demo
struct Demo_Previews: PreviewProvider {
static var previews: some View {
Text("Demo")
.textStyle(.title)
}
}
Prepared with Xcode 13.4 / iOS 15.5
Test module in GitHub

A View.environmentObject(_:) may be missing as an ancestor of this view - but not always…

I'm getting this error in production and can't find a way to reproduce it.
Fatal error > No ObservableObject of type PurchaseManager found. A
View.environmentObject(_:) for PurchaseManager may be missing as an
ancestor of this view. > PurchaseManager > SwiftUI
The crash comes from this view:
struct PaywallView: View {
#EnvironmentObject private var purchaseManager: PurchaseManager
var body: some View {
// Call to purchaseManager causing the crash
}
}
And this view is instantiated in subviews of the MainView
#main
struct MyApp: App {
let purchasesManager = PurchaseManager.shared
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(purchasesManager)
}
}
}
}
or, when called from a UIKit controller, from this controler:
final class PaywallHostingController: UIHostingController<AnyView> {
init() {
super.init(rootView:
AnyView(
PaywallView()
.environmentObject(PurchaseManager.shared)
)
)
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I tested all the use cases that trigger the PaywallView to show up, and I never got a crash.
FWIW, the PurchaseManager looks like this:
public class PurchaseManager: ObservableObject {
static let shared = PurchaseManager()
init() {
setupRevenueCat()
fetchOfferings()
refreshPurchaserInfo()
}
}
Why would the ObservableObject go missing? In which circumstances?
The reason your problem is intermittent, is probably because the PurchaseManager init()
could finish
before all the data is setup properly, due to the "delays" of the
async functions in init(). So sometimes the data will be available
when the View wants it, and sometimes it will not be there and crash your app.
You could try the following approach that includes #atultw advice of using
StateObject.
import SwiftUI
#main
struct TestApp: App {
#StateObject var purchaseManager = PurchaseManager() // <-- here
var body: some Scene {
WindowGroup {
MainView()
.onAppear {
purchaseManager.startMeUp() // <-- here
}
.environmentObject(purchaseManager)
}
}
}
struct MainView: View {
#EnvironmentObject var purchaseManager: PurchaseManager
var body: some View {
Text("testing")
List {
ForEach(purchaseManager.offerings, id: \.self) { offer in
Text(offer)
}
}
}
}
public class PurchaseManager: ObservableObject {
#Published var offerings: [String] = []
// -- here --
func startMeUp() {
// setupRevenueCat()
fetchOfferings()
// refreshPurchaserInfo()
}
func fetchOfferings() {
DispatchQueue.main.asyncAfter(deadline: .now()+2) {
self.offerings = ["offer 1","offer 2","offer 3","offer 4"]
}
}
}
Try not to use the singleton pattern here (.shared), EnvironmentObject is meant to be a replacement for it. You should instantiate PurchasesManager in MyApp.
#main
struct MyApp: App {
#StateObject var purchasesManager = PurchaseManager()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(purchasesManager)
}
}
}
}
without state object compiles fine but needed if you want child views to update automatically.
Doing those things with a dummy PurchasesManager runs fine for me.

Is there a SwiftUI equivalent to WKInterfaceController's .becomeCurrentPage method?

Previously in WatchKit we could tell a certain InterfaceController to present itself using .becomeCurrentPage, how can we do it in Swift UI?
In WatchKit for example I would:
// handle notification
#objc func respondToWaterlock(_ notification: NSNotification) {
self.becomeCurrentPage()
}
there is no equivalent for becomeCurrentPage method in SwiftUI. You can update your State or ViewModel to achieve a similar result.
for example:
enum Pages {
case home
case settings
}
struct MyView: View {
#State var selectedPage: Pages = .home
var body: some View {
Group {
if self.selectedPage == .home {
Text("Home")
} else if self.selectedPage == .settings {
Text("Settings")
}
}
}
}
You should just update the selectedPage state variable to change the page.

Resources